Answer to Wrong function called
The first step to understand this problem is to identify the undefined behaviour. It is actually hidden inside the variable argument list. What happens is more or less this:
Text o("a");
void *t = (void *)o;
Printable *p = (Printable *)t;
It is actually undefined what happens when you cast a void * to something
else than what you created the void * from, even though you could argue that
the pointer was originally a Printable. This is however not the case, and
to understand why this happens we require some insight into how virtual function calls
and multiple inheritance is implemented in C++.
To implement virtual function calls, the compiler will insert a pointer to a vtable as
the (usually) first member of your class. The vtable is more or less a list of pointers
to the virtual functions for this instance. For example, in the case of the Printable
object, the compiler will know that entry 1 (for example) is the pointer to the function
that overloads the toS method.
This is fairly easy to understand when not having multiple inheritance. If a subclass wants to add more virtual functions, it can just extend the vtable and add new entries there. The base class is not aware of them, so it does not hurt any code expecting a base class. However, how do we do when dealing with multiple inheritance?
This is where things get complicated. We have to somehow create an object that looks like
the Printable class sometimes, and the StrType class sometimes. This
is because when some code wants to call the toS in a Printable object,
the generated code will call the function located at the first position in the vtable. However,
generated code for calling type in the StrType class will also look at
the first position. How do we solve this?
The solution is to create multiple vtables and sometimes alter the pointer to the object. In this
case, the Text object will look something like this:
| Offset (32-bit systems) | Data |
|---|---|
| 0 | vtable compatible with StrType |
| - | all members in StrType (none present here) |
| 4 | vtable compatible with Printable |
| - | all members in Printable (none present here) |
| 8 | the text member |
So what happens when we cast from a Text* to a Printable* via a void*
is that we will end up with machine code that does not take the fact that we need to alter the pointer by
4 bytes in this case, so we end up getting an object that looks like a StrType object instead.
After that, we're just lucky that the function type has the same signature as toS,
otherwise our program would most certainly have crashed somehow.
To make the code work as expected, we have a few options:
- We could reorder the base classes for our
Textobject, so thatPrintableis the first base class. This elliminates the need for adjusting the pointer in this case. It would also work to remove theStrTypebase class. - We could rewrite the
myPrintfunction to use variadic templates present in C++11. This would keep the type information so that the compiler can generate correct code.
Comments
New comment
You can use GitHub flavored markdown here. Parsed by Parsedown, which does not support all of GitHub's features. For example, specifying the language of code listings is not supported.