Skip to main content
gamedev.net
🔒 Locked

Inheritance and subclass conversion constructors

Started by Mage2k 15 replies 2.1k views
Original Post
Mage2k
Mage2k
Consider the following (simplified) situation:

class Super
{
  public:
    A();
    virtual ~A();
};

class SubB;

class SubA: public Super
{
  public:
    SubA();
    SubA(const SubA& s);
    SubA(const SubB& s);
    ~SubA();
};

class SubB : public Super
{
  public:
    SubB();
    SubB(const SubA& s);
    SubB(const SubB& s);
    ~SubB();
};

//in later code using these classes
Super* s1 = new SubA();
Super* s2 = new SubB(*s1);



Now, the problem here is that you can't (at least, as far as I know) implicitly downcast, as is done here. So, given that I am working with superclass pointers, what is the best way to get this way working? One idea I've had is to do a checked dynamic_cast on s2 before calling the constructor, but using dynamic_cast in that way seems to defeat the polymorphism point, Basically, I want to use two constructors as copy and conversion constructors, respectively. Note that the same issue comes up when overloading the assignment operator for both types, e.g. (with a possible solution)

const Super& SubA::operator=(const SubA& s);
const Super& SubA::operator=(const SubB& s);

//although this would be transparent from the outside
const Super& SubA::operator=(const Super* s){
    SubA* a;
    if(a = dynamic_cast<SubA*>(s))
        *this = *a;
    else{
        SubB* b;
        b = dynamic_cast<SubB*>(s);
        *this = *b;
    }
}



However, is this a sensible solution, or is there a more elegant way of doing it? Also, I'm not sure about doing that in a constructor. Mage [Edited by - Mage2k on September 17, 2005 2:53:36 AM]
---------------------------------------------------There are 10 kinds of people in the world:Those that understand binary, and those that dont...Mage
Rayno
Rayno
Well one idea would be to have two virtual functions under Super.

class Super{  public:    A();    virtual ~A();    virtual SubA* MakeSubA();    virtual SubB* MakeSubB();};

And override them in SubA and SubB to do whatever needed.

And then just do.
Super* s1 = new SubA();Super* s2 = s1->MakeSubB();


But maybe thats too inconvenient.

edit:
Err..Actually they would have to return pointers. Which would be even more inconieniant.

edit2:
fixed.
Fruny
Fruny
You can probably get away with defining a conversion operator. It should solve some (though not all, unfortunately) of your problems. Note that you can't have both a "A to B" conversion operator and a "B from A" conversion constructor, since it leads to ambiguity.

The Visitor patter may probably help you with the assignment, though not with the construction; at least, not 'properly'. You may reassign members in the constructor's body based on something you do with the visitor pattern, but you probably won't be able to use the initializer list without being ... creative (virtual functions behave non-polymorphically in constructors).

Wont compile, see my next post
class Super{protected:   virtual void AssignTo(SubA&) const = 0;   virtual void AssignTo(SubB&) const = 0;};class SubA{public:   SubA& operator=(const Super& rhs) { rhs.AssignTo(*this); }};


Depending on who the rest of your code is structured, a Clone function may always come in handy:

class Super{public:   virtual Super* Clone() = 0;};class SubA{public:   SubA* Clone() { return new SubA(*this); }   // Or, if your compiler does not support covariant return (VC6)   // Super* Clone() { return new SubA(*this); }};


If all that were to fail to help you, educate yourself on "Multiple Dispatch".

And yes, dynamic_cast is the correct solution when a cross-cast is really needed.

[Edited by - Fruny on September 17, 2005 12:29:58 AM]
"Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it." — Brian W. Kernighan
Rayno
Rayno
Ok, Fruny's version is better and safer than mine. [wink]
Fruny
Fruny
#include <iostream>using namespace std;class SubA;class SubB;class Super{public:    virtual Super& operator=(const Super&) = 0;    virtual void AssignTo(SubA&) const = 0;    virtual void AssignTo(SubB&) const = 0;};class SubA : public Super{    void AssignTo(SubA& lhs) const     {     	cout << "SubA::AssignTo(SubA&)" << endl;     	lhs = *this;     }    void AssignTo(SubB& lhs) const;public:    SubA& operator=(const Super& rhs);    SubA& operator=(const SubA& rhs)      {     	cout << "SubA::operator=(SubA&)" << endl;     	return *this;     }        SubA& operator=(const SubB& rhs)      {     	cout << "SubA::operator=(SubB&)" << endl;     	return *this;     }};class SubB : public Super{    void AssignTo(SubA& lhs) const;    void AssignTo(SubB& lhs) const     {         cout << "SubB::AssignTo(SubA&)" << endl;         lhs = *this;     }    public:    SubB& operator=(const Super& rhs);        SubB& operator=(const SubA& rhs)      {     	cout << "SubB::operator=(SubA&)" << endl;     	return *this;     }        SubB& operator=(const SubB& rhs)      {     	cout << "SubB::operator=(SubB&)" << endl;     	return *this;     }};// These functions need SubA and SubB to be complete types// They will have to go in the appropriate .cpp filesvoid SubA::AssignTo(SubB& lhs) const{	cout << "SubA::AssignTo(SubB&)" << endl; 	lhs = *this;}SubA& SubA::operator=(const Super& rhs) { 	cout << "SubA::operator=(Super&)" << endl; 	rhs.AssignTo(*this); 	return *this;}void SubB::AssignTo(SubA& lhs) const{	cout << "SubB::AssignTo(SubA&)" << endl; 	lhs = *this;}SubB& SubB::operator=(const Super& rhs) { 	cout << "SubB::operator=(Super&)" << endl; 	rhs.AssignTo(*this); 	return *this;}int main(){   Super* a = new SubA;   Super* b = new SubB;      cout << "***" << endl;   *a = *b;   cout << "***" << endl;}


After working out the kinks in the code I posted earlier...
"Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it." — Brian W. Kernighan
Mage2k
Mage2k
Well, what about doing something like this to exploit covariant return?

class Super{public:  virtual Super* getTypedPointer() = 0;}class SubA : public Super{public:  SubA(const SubA& s);  SubA(const SubB& s);  SubA* getTypedPointer(){    return this;  }};//same for SubB//thenSuper* s1 = new SubA();Super* s2 = new SubB(*(s1->getTypedPointer()));


Can I do that with dereference (*) operator? If not, I could change the function parameters to pointers..

Mage
---------------------------------------------------There are 10 kinds of people in the world:Those that understand binary, and those that dont...Mage
Fruny
Fruny
Quote:
Original post by Mage2k
Well, what about doing something like this to exploit covariant return?


No.

Super* s = new SubA;SubA* a = new SubA;


s->getTypedPointer() will return a Super* pointing to a SubA object.
a->getTypedPointer() will return a SubA* pointing to a SubA object.

Covariant return doesn't affect polymorphic use. Instead, it avoids you getting pointers and references to base-class objects when you are manipulating the object through its real interface.
"Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it." — Brian W. Kernighan
Mage2k
Mage2k
Quote:
Original post by Fruny
Quote:
Original post by Mage2k
Well, what about doing something like this to exploit covariant return?


No.

Super* s = new SubA;SubA* a = new SubA;


s->getTypedPointer() will return a Super* pointing to a SubA object.
How will it return a Super* when the base class getTypedPointer() is declared abstract?
---------------------------------------------------There are 10 kinds of people in the world:Those that understand binary, and those that dont...Mage
Polymorphic OOP
Polymorphic OOP
Quote:
Original post by Mage2k
How will it return a Super* when the base class getTypedPointer() is declared abstract?

So what? If you call the function from a pointer to the base class, then that is the return-type which will be used. C++ is statically typed -- you only get the return value of the child type if you have a pointer to the child type or one of its children.
Fruny
Fruny
Quote:
Original post by Mage2k
How will it return a Super* when the base class getTypedPointer() is declared abstract?


With polymorphism, the type of the pointer and the type of the pointee are two different things. The type of the pointer (static type) is fixed by the variable definition or, here, by the function's return type. The type of the pointee (dynamic type) is fixed by whatever you passed to new or whatever the real type of the object is.

With covariant returns, instead of imposing that the function return types be all identical, you are allowed override the base function with versions that differ in their return type, so long as that return type that derive from the type that was returned in the base class. Now, the implementation of the function will be selected according to the dynamic type of the object whose member function you're calling, so that behaviour be appropriate, but the static type of the returned object will be selected according to the static type of the object you are using the member function through.

Super* s = new SubA; s->getTypedPointer(); // calls SubA::getTypedPointer, returns a Super* that points to a SubASuper* s = new SubB; s->getTypedPointer(); // calls SubB::getTypedPointer, returns a Super* that points to a SubBSubA*  s = new SubA; s->getTypedPointer(); // calls SubA::getTypedPointer, returns a SubA*  that points to a SubASubB*  s = new SubB; s->getTypedPointer(); // calls SubB::getTypedPointer, returns a SubB*  that points to a SubB
"Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it." — Brian W. Kernighan
Mage2k
Mage2k
Gotcha, makes sense, but it seems kinda counter-intuitive.
---------------------------------------------------There are 10 kinds of people in the world:Those that understand binary, and those that dont...Mage
Fruny
Fruny
Quote:
Original post by Mage2k
Gotcha, makes sense, but it seems kinda counter-intuitive.


Ok, let's try it that way: when you use a SubA*, the whole SubA interface is available. SubA::getTypedPointer() is defined as returning a SubA*, so that's what the return type is (this doesn't affect what the pointer actually points to).

When you use a Super*, only the base class interface is available (you mught not even have included the header that defined SubA). In that interface, getTypedPointer() is defined as returning a Super*, so again, that's what you get (still without affecting the object it actually points to).

When you compile a program, the compiler cannot trace the dynamic type of objects across function boundaries since that's decided at tun-time. The only thing it can (and should) rely on are the static types as declared in the program to verify that your code is legal. Just because you do Super* s = new SubA; doesn't mean you can use a as if it were a full SubA object: you are limited to the Super interface (even though the compiler might do some static analysis, find out that the object a points to cannot be anything else but a SubA and thus turn a virtual function call into a non-virtual function call - as an optimization).
"Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it." — Brian W. Kernighan
Mage2k
Mage2k
Ok, now that makes a lot more sense: the issue is the interface available at compile-time whereas polymorphism is a run-time feature. Similar to how in the situation where a subclass adds to the base classes interface, you need a reference to the subclass in order to call those methods as they don't exist for a reference to the superclass. Thank you. This is one of the things I love about the language: there's always some new little detail, always with a purpose, and always with a damn good reason that it works the way it does.

Mage
---------------------------------------------------There are 10 kinds of people in the world:Those that understand binary, and those that dont...Mage
Shannon Barber
Shannon Barber
If the sub-classes are different, how can they be so easily converted between each other? Seems like they should be the same class and/or this is not a good case for inheritence/polymorphism. It violates Liskov's substitution principle.
The trade-off between price and quality does not exist in Japan. Rather, the idea that high quality brings on cost reduction is widely accepted.-- Tajima & Matsubara
Mage2k
Mage2k
Quote:
Original post by Shannon Barber
If the sub-classes are different, how can they be so easily converted between each other? Seems like they should be the same class and/or this is not a good case for inheritence/polymorphism. It violates Liskov's substitution principle.
Base Class => Transform
SubClassA => Matrix3DTransform
SubClassB => QuaternionTransform

Each subclass shares the exact same interface and a Vector3D for translation. However, each represents the rotation differently, but I would like to be able to convert between the two (as it is very easy to convert between a quaternion and 3x3 matrix). I may even add a Matrix4DTransform class, but that would entail writing Vector4D and Matrix4D classes (you may ask, why not templatize? Because, in this case, I'd prefer the extra (grunt) work of writing out the classes for readability's sake).

---------------------------------------------------There are 10 kinds of people in the world:Those that understand binary, and those that dont...Mage
Glak
Glak
vector and list both implement begin(), end(), push_back(), size(), etc... and yet they do not share a base class. Sometimes theoretical elegance and get in the way of real elegance.
Polymorphic OOP
Polymorphic OOP
Quote:
Original post by Glak
vector and list both implement begin(), end(), push_back(), size(), etc... and yet they do not share a base class. Sometimes theoretical elegance and get in the way of real elegance.

Agreed. Given the situation, it is very unlikely that you need the runtime polymorphism that virtual functions in a hierarchy provide. More likely you just want your types to share a common interface. In that case, you should let them just have their functions have the same name and perform the same logical operation, and then use templates to interface with them in the appropriate contexts.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.