SRL Archive - Lape

Question about super-types and sub-types


Gu3st...

2014-04-08 19:24:58

@Brandon; @nielsie95; @Dgby714; Is this a bug? If it is a bug then it'll explain all my questions, but if it's not a bug then I have more questions…

type
T3DPoint = record
X, Y, Z : Extended;
end;
T3DColorPoint = record(T3DPoint)
color : Integer;
end;

procedure T3DPoint.create(pX, pY, pZ : Extended);
begin
self.X := pX;
self.Y := pY;
self.Z := pZ;
end;

procedure T3DColorPoint.create(pX, pY, pZ : Extended; pColor : Integer); overload;
begin
self.create(pX, pY, pZ);
self.color := pColor;
end;

var
P : T3DPoint;
cP : T3DColorPoint;

begin
P.create(1.0, 1.0, 1.0); // Exception in Script: Unknown declaration "create"
cP.create(1.0, 1.0, 1.0, 1);
writeLn(P, ' ', cP);
end;

Change T3DColorPoint.create to this and it works, but this kind of defeats the purpose of having T3DColorPoint as a sub-type of T3DPoint :
procedure T3DColorPoint.create(pX, pY, pZ : Extended; pColor : Integer);
begin
self.X := pX;
self.Y := pY;
self.Z := pZ;
self.color := pColor;
end;

Brandon

2014-04-08 20:09:02

To be completely honest, I'm not sure if that's a bug or if it is intended.. I am going to say it is a bug for the same reasons that parameters and object fields are in the same scope (explained below).. The reason I say this is because unlike C++ or Java, Lape does NOT have virtual functions in the same way.. In other words, the derived class cannot access the parent class's functions that were overloaded. It is most likely due to the fact that lape does not have static functions or even virtual lookup..

Brace yourself for an explanation..

Example:


class T3DPoint //Base class.
{
private:
float x, y, z;

public:
T3DPoint(float X, float Y, float Z) : x(X), y(Y), z(Z) {} //Base class constructor taking 3 parameters.
};

class T3DColourPoint : public T3DPoint //Derived class inheriting from Base class.
{
private:
int colour;

public:
T3DColorPoint(float X, float Y, float Z, int Colour) : T3DPoint(X, Y, Z), colour(Colour) {} //call the super constructor then initialize our own variables.. aka colour..
};

As you can see, we can call the base class's constructor explicitly. In java this is equivalent to the super keyword.

What is the equivalent in Lape then? Let's think about this for a second.

In Lape, we have self keyword to access the current instance of an object:

Bad Code (parameters and objects share the same scope):

type
T3DPoint = record
X, Y, Z : Extended;
end;

procedure T3DPoint.create(X, Y, Z : Extended);
begin
self.X := X;
self.Y := Y;
self.Z := Z;
end;

But does the self keyword really work like the this keyword? No it does not because as you can see above, the parameters are named the same as the object's fields and yet self.X is assigning to itself! It's not actually assigning parameter X to self.X. This was discussed previously and I deemed it as a bug..


The same can be said for functions especially overloaded ones. So how do we call the base class's overloaded function? The idea is to NOT overload! When you overload a function, you create what is called a virtual function and its address is added to what is called the Virtual table or VTable for short.. This allows polymorphism to work.. http://en.wikipedia.org/wiki/Virtual_method_table

When the derived class's function is called, it is looked up in the VTable to make sure that it has the right one. If it is NOT overloaded, it isn't added to the VTable and thus no lookup occurs.

It immediately calls the derived one instead of having to decide whether the user meant derived or base's function. Some languages use a dispatch method to decide which to call. Usually lookup occurs when trying to decide which function to call from a base pointer to a derived instance.. Anyway, enough rambling..

If we do NOT overload the function, then we must decide for ourselves which one to call.. We do this manually. Now we know that the derived class inherits from the base class and so they shared the same memory address! Using this and the fact that we can cast (dynamic_cast) one to the other, we can cast a pointer to derived to a pointer to base and call the function through that..


Solution:


type
T3DPoint = record
X, Y, Z : Extended;
end;
T3DColorPoint = record(T3DPoint)
color : Integer;
end;

procedure T3DPoint.create(pX, pY, pZ : Extended);
begin
self.X := pX;
self.Y := pY;
self.Z := pZ;
end;

procedure T3DColorPoint.create(pX, pY, pZ : Extended; pColor : Integer);
type TP = ^T3DPoint;
begin
TP(@self)^.create(pX, pY, pZ); //Cast a pointer to "self (derived)" to a pointer to "base" and call 'create' through the "base" pointer.
//In C++, this function would be an overload and virtual and thus a lookup would occur due to calling create through the base pointer.
//That's basically what I was trying to explain in that long paragraph above..
self.color := pColor;
end;

var
P : T3DPoint;
cP : T3DColorPoint;

begin
P.create(1.0, 2.0, 3.0);
cP.create(3.0, 5.0, 7.0, 9);
writeLn(P, ' ', cP);
end;


Voila for workarounds.. This is NOT a hack. It's a very good "trick" with good solid proof.. It is usually done in C++ (automatically or manually). I never thought I'd see it in Lape.. Seeing as lape has pointers and stuff, you might be seeing a lot of C and C++ techniques leak into lape.


NOTE: Don't cast or assign derived to base directly. It must be a pointer (all pointers are the same size). If you do it directly without pointers, derived is larger than base and you may actually introduce a problem known as slicing: http://stackoverflow.com/questions/274626/what-is-the-slicing-problem-in-c

nielsie95

2014-04-09 04:42:41

This is a bug. You make some good points, Brandon, but this can be resolved statically, so no VMT is needed. What you are talking about is not overloading, it is overriding ;)

You point out a very nice workaround, though!

Gu3st...

2014-04-09 07:52:01

Thanks a lot, Brandon :) I just spent about 30 minutes reading various pages about polymorphism, object slicing and the D language.
This fix makes the code very much not optimal for newcomers though (bit cluttered and not obvious what we want the compiler to do), but it's good enough.

I'm thinking about just scraping the idea of having T3DColorPoint be a sub-class of T3DPoint and just adding a point member to the T3DColorPoint type instead; it would get rid of the cluttered code while maintaining functionality.

What about this, then:
type
T3DPoint = record
X, Y, Z : Extended;
end;
P3DPoint = ^T3DPoint;

T3DColorPoint = record(T3DPoint)
color : Integer;
end;
P3DColorPoint = ^T3DColorPoint;

function T3DPoint.equals(P : T3DPoint): Boolean;
begin
if self.X = P.X then
if self.Y = P.Y then
result := self.Z = P.Z;
end;

function T3DColorPoint.equals(P : T3DColorPoint): Boolean;
begin
if P3DPoint(@self)^.equals(P3DPoint(@P)^) then
result := self.color = P.color;
end;

var
P : T3DPoint;
cP : T3DColorPoint;

begin
cP.color := 1;
writeLn(P.equals(P3DPoint(@cP)^)); // True, expected
writeLn(cP.equals(P3DColorPoint(@P)^)); // False, expected
//writeLn(cP.equals(P)); // Not allowed. Why not?
end;

E: I've learned so much new stuff about pointers by asking you these kind of questions, @Brandon;. I think I want to marry you :o

Brandon

2014-04-09 09:19:00

Thanks for the corrections Nelsie. Really appreciate it. Zytex was originally overloading though? You are probably right though. Glad I could help too.

Anyway:
From: Zyt3x


What about this, then:
type
T3DPoint = record
X, Y, Z : Extended;
end;
P3DPoint = ^T3DPoint;

T3DColorPoint = record(T3DPoint)
color : Integer;
end;
P3DColorPoint = ^T3DColorPoint;

function T3DPoint.equals(P : T3DPoint): Boolean;
begin
if self.X = P.X then
if self.Y = P.Y then
result := self.Z = P.Z;
end;

function T3DColorPoint.equals(P : T3DColorPoint): Boolean;
begin
if P3DPoint(@self)^.equals(P3DPoint(@P)^) then
result := self.color = P.color;
end;

var
P : T3DPoint;
cP : T3DColorPoint;

begin
cP.color := 1;
writeLn(P.equals(P3DPoint(@cP)^)); //Good. Legal.
writeLn(cP.equals(P3DColorPoint(@P)^)); //Bad. Illegal. Undefined. Possible access violation.
//writeLn(cP.equals(P)); //CP is both a T3DPoint and a T3DColorPoint. P is only a T3DPoint NOT a T3DColorPoint. P does NOT inherit. CP does.
end;


Undefined behaviour. P3DColorPoint is a type of P3DPoint because it derives from it. However the other way around is NOT true.

Casting a T3DPoint to a T3DColorPoint is undefined behaviour because a T3DColorPoint is larger and contains an extra field.

Think about it like this:


Now imagine casting a T3DColorPoint to a T3DPoint. Which fields do you now only have access to? You have access to only X, Y, Z. Information about the colour field is gone. Remember, since we are originally a T3DColorPoint, we allocated space for 4 fields: X, Y, Z, Colour. We are only accessing 3 of those by casting. The fourth is still there (it's memory is still available and we can cast back to access it), it's just invisible now.


Now imagine casting a T3DPoint to a T3DColorPoint. What fields do you now have access to? You allocated space for 3 fields: X, Y, Z. And now you're trying to access a fourth field which does NOT exist for a T3DPoint: Colour.

Why doesn't it exist? Because you only had 3 fields in the first place and not four. You cannot acquire the fourth by casting. You can lose fields but never gain.


Casting from child to parent is known as up-casting. A child "IS-A" type of parent. That's the relationship it has (IS-A relationship).
Casting from parent to child is known as down-casting. A parent is NOT a type of child. It does not inherit from child (cyclical inheritance). The relationship is has is nothing.. It's just a parent.



var
P: P3DPoint;
CP: T3DColorPoint := [1, 2, 3, 7];
begin
P := P3DPoint(@CP); //up-cast child to parent.
writeln(P3DColorPoint(P)^.Color); //down-cast "back-to" child.
//Legal/defined because we were originally a child.
end.

Gu3st...

2014-04-09 12:59:42

From: Brandon
Thanks for the corrections Nelsie. Really appreciate it. Zytex was originally overloading though? You are probably right though. Glad I could help too.

Anyway:


Undefined behaviour. P3DColorPoint is a type of P3DPoint because it derives from it. However the other way around is NOT true.

Casting a T3DPoint to a T3DColorPoint is undefined behaviour because a T3DColorPoint is larger and contains an extra field.

Think about it like this:


Now imagine casting a T3DColorPoint to a T3DPoint. Which fields do you now only have access to? You have access to only X, Y, Z. Information about the colour field is gone. Remember, since we are originally a T3DColorPoint, we allocated space for 4 fields: X, Y, Z, Colour. We are only accessing 3 of those by casting. The fourth is still there (it's memory is still available and we can cast back to access it), it's just invisible now.


Now imagine casting a T3DPoint to a T3DColorPoint. What fields do you now have access to? You allocated space for 3 fields: X, Y, Z. And now you're trying to access a fourth field which does NOT exist for a T3DPoint: Colour.

Why doesn't it exist? Because you only had 3 fields in the first place and not four. You cannot acquire the fourth by casting. You can lose fields but never gain.


Casting from child to parent is known as up-casting. A child "IS-A" type of parent. That's the relationship it has (IS-A relationship).
Casting from parent to child is known as down-casting. A parent is NOT a type of child. It does not inherit from child (cyclical inheritance). The relationship is has is nothing.. It's just a parent.



var
P: P3DPoint;
CP: T3DColorPoint := [1, 2, 3, 7];
begin
P := P3DPoint(@CP); //up-cast child to parent.
writeln(P3DColorPoint(P)^.Color); //down-cast "back-to" child.
//Legal/defined because we were originally a child.
end.
Yeah, I know this but I though it would just cast X, Y and Z to the function (basically just doing "P3DPoint(@cP)^" automatically for me.
Oh well :p