Jump to content
  • Advertisement
Sign in to follow this  
Gumgo

Multiple inheritance and casting...

This topic is 3087 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

I have the following situation. I have a scene graph with nodes, and each type of node is derived from a base class which provides basic node functionality:

class SGNode {
...
};

To implement specific functionality, I have other classes the nodes also are derived from; example:

class RenderDataMesh {
...
};

class NodeDataMaterial {
};

An SGNodeMaterial and SGNodeMesh might look like this:

class SGNodeMaterial : public SGNode, public NodeDataMaterial {
};

class SGNodeMesh : public SGNode, public RenderDataMesh {
const NodeDataMaterial * materialData;
...
};

As you can see, the mesh node stores a pointer to a NodeDataMaterial instance. Upon construction, the following occurs within the SGNodeMesh:

SGNodeMesh::SGNodeMesh( SGNode * parent ) : SGNode( parent ), RenderDataMesh() {
nodeType = SGNode::NODE_RENDER_DATA_MESH;

// search up the tree to link to the necessary nodes
bool materialFound = false;
SGNode * prevNode = parent;
while (prevNode != NULL && !materialFound) {
if (prevNode->getNodeType() == SGNode::NODE_MATERIAL && !materialFound) {
material = (NodeDataMaterial*)prevNode;
materialFound = true;
} else
prevNode = prevNode->getParent();
}
}



However, I just now noticed an issue with this:

material = (NodeDataMaterial*)prevNode;

Since prevNode is just an SGNode pointer, I can't do this, can I? I'd have to first cast up to SGNodeMaterial, then down to NodeDataMaterial:

material = (NodeDataMaterial*)((SGNodeMaterial*)prevNode);

That's pretty ugly. Is this correct, and is there another nicer way of doing this? Thanks.

Share this post


Link to post
Share on other sites
Advertisement
Quote:
However, I just now noticed an issue with this:
material = (NodeDataMaterial*)prevNode;

Since prevNode is just an SGNode pointer, I can't do this, can I? I'd have to first cast up to SGNodeMaterial, then down to NodeDataMaterial:
material = (NodeDataMaterial*)((SGNodeMaterial*)prevNode);
Both of those are bad in C++. That's a C cast, which lets you cast any pointer to any other pointer whether it's valid or not.

You should be using the C++ cast:
material = static_cast<NodeDataMaterial*>(prevNode);

static_cast will figure out the correct offsets (going up/down the hierarchy), and if it can't figure it out, it will let you know with a compile error (wheras the C-cast will compile and then give you invalid results at runtime).

Share this post


Link to post
Share on other sites
Ah, thanks, I didn't know this about static_cast.
While on the topic of C++ casting, I've heard it's best to avoid the RTTI that comes along with dynamic_cast if possible (I might be wrong). Can you explain the difference between how static_cast works and how dynamic_cast works in this case?

Share this post


Link to post
Share on other sites
static_cast is resolved at compile time. It's pretty much the same as traditional C casts, except it refuses to compile if you specify an invalid cast.

e.g. in your case it might compile to something like
byte* addressA = ...
byte* addressB = addressA - 4;

dynamic_cast is resolved at run-time (hence, it's slow). It relies on RTTI to determine the type of the object passed to it, and finds the correct offset depending on the target-type, and can even fail if the cast is invalid. You can only use dynamic_cast on objects that have virtual functions.

reinterpret_cast is your dirty backup when you actually do want to do an invalid cast. This is the same as a C casts in that it doesn't complain about bad casts, but by writing something ugly like reinterpret_cast you're commenting your code and saying "I know what I'm doing here".
e.g. you use this for black magic
int i = 0xbaadf00d;
float f = *reinterpret_cast<float*>(&i);

Share this post


Link to post
Share on other sites
Note that it is totally possible to specify a bad static_cast:

struct Base1
{
virtual ~Base1() { }
virtual void Foo() { }
};

struct Base2
{
virtual ~Base2() { }
virtual void Bar() { }
};

struct Derived : public Base1, public Base2
{
virtual void Quux() { }
};


int main()
{
Base2* baseobject = new Base2;
Derived* bogus = static_cast<Derived*>(baseobject);
bogus->Quux();
delete baseobject;
}



This will vomit at runtime, because all static_cast does is check that the types you provide might feasibly be related.

If you have a safety check to ensure that your cast will provide a valid result, you can safely ignore this caveat. However, it's a fragile thing to do, and can potentially introduce some very subtle and very, very ugly bugs.


Contrast that with this variant relying on dynamic_cast:

int main()
{
Base2* baseobject = new Base2;
Derived* bogus = dynamic_cast<Derived*>(baseobject);
if(bogus)
bogus->Quux();
delete baseobject;
}


This one won't crash, because the dynamic_cast will fail and return NULL, and the if() check will therefore ignore the bogus object.

The difference is that dynamic_cast will actually look at the object you provide it rather than just the class types you provide. Since it uses the object instance, it can guarantee that the type conversion is valid. However, this requires runtime investigation of the object itself to do, hence "dynamic" cast.


There's a lot of BS rumors being spread around about how dynamic_cast is slow and blah blah blah. This is largely pointless to worry about. Unless you are doing literally millions of casts per frame, you will never run into a substantial speed problem, and the extra safety of using the dynamic checks will save you a huge amount of headache with bugs and undefined behavior nastiness. When in doubt, rely on dynamic_cast for safety first and foremost, and then if and only if your profiler indicates that your casting is a speed issue, look for faster alternatives.

Note that writing your own RTTI substitute isn't likely to be much (if any) of a win over the standard library implementation, unless you really, really, really know what you're doing. If you find yourself in a situation where dynamic_casting is killing your performance, your best bet is to make an algorithmic level change and eliminate the need to cast in the first place, rather than try to risk unsafe casting or rolling your own "fast" dynamic cast.

Share this post


Link to post
Share on other sites
Ah, that makes sense. So, the issue I see then is this (ignoring virtual destructors):

class Base {
public:
bool has1, has2, has3;

Base() {
has1 = false;
has2 = false;
has3 = false;
}
};

class Property1 {
};

class Property2 {
};

class Property3 {
};

// derives properties 1 and 2
class Derived12 : public Base, public Property1, public Property2 {
public:
Derived12() : Base() {
has1 = true;
has2 = true;
}
};

// derives properties 2 and 3
class Derived23 : public Base, public Property2, public Property3 {
public:
Derived23() : Base() {
has2 = true;
has3 = true;
}
};

Base * instances = <a bunch of instances of Derived12 and Derived23>;

Base * base = instances[something];
if (base->has2)
Property2 * prop2 = static_cast <Property2*>( base ); // This line does not make sense to me



At runtime, the pointer base could be Base, Derived12, or Derived23. The programmer has added has1, has2, and has3 to keep track of this. I don't see how the base pointer could possibly be cast from Base to Property2, even if the programmer uses reinterpret_cast, or just a C cast. The reason is because, afaik, the location of Property2 relative to Derived12's starting address isn't the same as Property2's location relative to Derived23's starting address. What I mean is:

+----+ +---------+ +---------+ | +----+ +---------+ +---------+
|Base| |Property1| |Property2| | |Base| |Property2| |Property3|
+----+ +---------+ +---------+ | +----+ +---------+ +---------+
| | | | | | |
+--------+-----------+ | +--------+-----------+
| | |
v | v
+---------+ | +---------+
|Derived12| | |Derived23|
+---------+ | +---------+
|
Might look like: | Might look like:
|
+ADDRESS--+ | +ADDRESS--+
|Base | | |Base |
+---------+ Not the same | +---------+
|Property1| +---------------->|Property2|
+---------+ | offset! | +---------+
|Property2|<---+ | |Property3|
+---------+ | +---------+

Does that make sense? So basically, the only way to ensure this:

+-----+ cast +-----+
|Base1| ----> |Base2|
+-----+ +-----+
| |
+------+------+
|
v
+-------+
|Derived|
+-------+

Is to first cast from Base1 to Derived, then from Derived to Base2, right?

Share this post


Link to post
Share on other sites
There's just one mistake in your logic: casting a pointer is not the same thing as just treating the raw address as a different type, even conceptually. Casting a pointer is instructing the compiler to look up the correct conversion for you; the idea is that all the juggling of various addresses and such is all done magically behind the scenes. You don't need to do any weird stuff like cast down to the base and then back up, because that's done automatically.

In your example, the compiler knows how Derived12 and Derived23 are laid out in memory. It knows that in order to get to Property2 in Derived12, it has to offset to one place; whereas in order to get to Property2 in Derived23, it has to use a different offset. When you tell it to convert from a Base to a Derived12 using static_cast, the compiler will check that Derived12 is indeed a subtype of Base, and then silently assume that you know what you're doing, and generate the code that uses the first offset. If you instead told it to static_cast to a Derived23, the compiler will instead just generate the code for the second offset.

This is why it is dangerous to rely on static_cast exclusively in multiple-inheritance scenarios, as I touched in earlier. To continue with your example, if you use a dynamic_cast instead, the compiler generates logic that roughly equates to this:

Look up the type information for the object being casted
If the type is Derived12
Use the offset information for the Derived12 class to get to the Property2 data
Else if the type is Derived23
Use the offset information for the Derived23 class to get to the Property2 data
Else
Return NULL because the cast can't be performed in this case


The caveat here is that the type information (usually handled as a "v-table" or "virtual dispatch table") is only attached to the object if the base class specifies at least one virtual method. However, in practice, the odds of using multiple inheritance and not using virtual dispatch are pretty darn low, because that just doesn't make much sense in a good design - so you don't have to worry too much about that [smile]

Share this post


Link to post
Share on other sites
Ah, sorry, I think I've described this wrong... or perhaps I'm just misunderstanding (thanks for the explanations btw). I'm not trying to cast from Base to Derived12 or Derived23. I'm trying to cast from Base to Property2. Both Base and Property2 contribute to Derived12 and Derived23. Hence, if I know that the object is actually a Derived12 or Derived23, I should be able to access Property2 somehow. However, it seems to me like the only way to do this would be to handle these cases separately; that is, I could NOT do something like this:

if (has_property2) prop2 = static_cast <Property2*>( base );

but instead, would need to do this:

if (is_derived12) prop2 = static_cast <Property2>( static_cast <Derived12>( base ) );
else if (is_derived23) prop2 = static_cast <Property2>( static_cast <Derived23>( base ) );

This is because the way the compiler accesses Property2 from Derived12 might be different than how it accesses Property2 from Derived23, and since there's no way to tell if the object is a Derived12 or Derived23, the compiler can't tell how to access Property2.

I mean, when casting to Derived12 and Derived23, I understand that the reason it can work is because the compiler "knows" how to convert from a Base to a Derived12 or Derived23, but you have to tell the compiler which one to do. But with a direct cast to Property2, this wouldn't work.

Does that make sense?

EDIT: Haha oops, sorry about posting that, because it's basically exactly what you said, ApochPiQ (somehow I missed reading the bottom part of your post!). So the two static_casts right now is basically doing the job of dynamic_cast. Thanks for the clarification.

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!