while trying to analyse in greater depth inheritance mechanism of C++ I stumbled upon the following example:
#include<iostream>
using namespace std;
class Base {
public:
virtual void f(){
cout << "Base.f" << endl;
}
};
class Left : public virtual Base {
};
class Right : public virtual Base{
public:
virtual void f(){
cout << "Right.f" << endl;
}
};
class Bottom : public Left, public Right{
};
int main(int argc,char **argv)
{
Bottom* b = new Bottom();
b->f();
}
The above, somehow, compiles and calls Right::f(). I see what might be going on in the compiler, that it understands that there is one shared Base object, and that Right overrides f(), but really, in my understanding, there should be two methods: Left::f()
(inherited from Base::f()
) and Right::f()
, which overrides Base::f()
. Now, I would think, based that there are two separate methods being inherited by Bottom, both with same signature, there should be a clash.
Could anyone explain which specification detail of C++ deals with this case and how it does it from the low-level perspective?
In the dreaded diamond there is a single base, from which the two intermediate objects derive and then the fourth type closes the diamond with multiple inheritance from both types in the intermediate levels.
Your question seems to be how many
f
functions are declared in the previous example? and the answer is one.Lets start with the simpler example of a linear hierarchy of just base and derived:
In this example there is a single
f
declared for which there are two overrides,base::f
andderived::f
. In an object of typederived
, the final overrider isderived::f
. It is important to note that bothf
functions represent a single function that has multiple implementations.Now, going back to the original example, on the line on the right,
Base::f
andRight::f
are in the same way the same function that is overridden. So for an object of typeRight
, the final overrider isRight::f
. Now for a final object of typeLeft
, the final overrider isBase::f
asLeft
does not override the function.When the diamond is closed, and because inheritance is
virtual
there is a singleBase
object, that declares a singlef
function. In the second level of inheritance,Right
overrides that function with its own implementation and that is the final overrider for the most derived typeBottom
.You might want to look at this outside of the standard and take a look at how this is actually implemented by compilers. The compiler, when creating the
Base
object it adds a hidden pointervptr
to the virtual table. The virtual table holds pointers to thunks (for simplicity just assume that the table held pointers to the function's final overriders, [1]). In this case, theBase
object will contain no member data and just a pointer to a table that holds a pointer to the functionBase::f
.When
Left
extendsBase
, a new vtable is created forLeft
and the pointer in that vtable is set to the final overrider off
at this level, which is incidentallyBase::f
so the pointers in both vtables (ignoring the trampolin) jump to the same actual implementation. When an object of typeLeft
is being constructed, theBase
subobject is initialized first, and then prior to initialization of the members ofLeft
(if there were) theBase::vptr
pointer is updated to refer toLeft::vtable
(i.e. the pointer stored inBase
refers to the table defined forLeft
).On the other side of the diamond, the vtable that is created for
Right
contains a single thunk that ends up callingRight::f
. If an object of typeRight
was to be created the same initialization process would happen and theBase::vptr
would point toDerived::f
.Now we get to the final object
Bottom
. Again, a vtable is generated for the typeBottom
and that vtable, as is the case in all others, contains a single entry that representsf
. The compiler analyzes the hierarchy of inheritance and determines thatRight::f
overridesBase::f
, and there is no equivalent override on the left branch, so inBottom
's vtable the pointer representingf
refers toRight::f
. Again, during construction of theBottom
object, theBase::vptr
is updated to refer toBottom
's vtable.As you see, all four vtables have a single entry for
f
, there is a singlef
in the program, even if the value stored in each vtable is different (the final overriders differ).[1] The thunk is a small piece of code that adapts the
this
pointer if needed (multiple inheritance usually implies it is needed) and then forwards the call to the actual override. In the event of single inheritance, thethis
pointer does not need to be updated and the thunk disappears, with the entry in the vtable pointing directly to the actual function.