I am trying to understand better the concept of virtual inheritance, and what are its perils.
I read in another post (Why is Default constructor called in virtual inheritance?) that it (= virtual inheritance) changes the order of constructor call (the "grandmother" is called first, while without virtual inheritance it doesn't).
So I tried the following to see that I got the idea (VS2013):
#define tracefunc printf(__FUNCTION__); printf("\r\n")
struct A
{
A(){ tracefunc; }
};
struct B1 : public A
{
B1(){ tracefunc; };
};
struct B2 : virtual public A
{
B2() { tracefunc; };
};
struct C1 : public B1
{
C1() { tracefunc; };
};
struct C2 : virtual public B2
{
C2() { tracefunc; };
};
int _tmain(int argc, _TCHAR* argv[])
{
A* pa1 = new C1();
A* pa2 = new C2();
}
The output is:
A::A
B1::B1
C1::C1
A::A
B2::B2
C2::C2
Which is not what I expected (I expected the order of the 2 classes will be different).
What am I missing? Can someone explain or direct me to a source that explains it better?
Thanks!
In your example, your output is to be expected. Virtual inheritance
comes into play in the instance when you have a class with multiple inheritance who's parent classes also inherit from the same class/type (i.e. the "diamond problem"). In your example, your classes might be set up to virtually inherit (if needed elsewhere in the code), but they don't necessarily 'virtually inherit' based on your example since none of the derived classes (B1/B2/C1/C2
) do more than inherit directly from A
.
To expand, I've tweaked your example to explain a little more:
#include <cstdio>
#define tracefunc printf(__FUNCTION__); printf("\r\n")
struct A
{
A() { tracefunc; }
virtual void write() { tracefunc; }
virtual void read() { tracefunc; }
};
struct B1 : public A
{
B1() { tracefunc; };
void read(){ tracefunc; }
};
struct C1 : public A
{
C1() { tracefunc; };
void write(){ tracefunc; }
};
struct B2 : virtual public A
{
B2() { tracefunc; };
void read(){ tracefunc; }
};
struct C2 : virtual public A
{
C2() { tracefunc; };
void write(){ tracefunc; }
};
// Z1 inherits from B1 and C1, both of which inherit from A; when a call is made to any
// of the base function (i.e. A::read or A::write) from the derived class, the call is
// ambiguous since B1 and C1 both have a 'copy' (i.e. vtable) for the A parent class.
struct Z1 : public B1, public C1
{
Z1() { tracefunc; }
};
// Z2 inherits from B2 and C2, both of which inherit from A virtually; note that Z2 doesn't
// need to inherit virtually from B2 or C2. Since B2 and C2 both virtual inherit from A, when
// they are constructed, only 1 copy of the base A class is made and the vtable pointer info
// is "shared" between the 2 base objects (B2 and C2), and the calls are no longer ambiguous
struct Z2 : public B2, public C2
{
Z2() { tracefunc; }
};
int _tmain(int argc, _TCHAR* argv[])
{
// gets 2 "copies" of the 'A' base since 'B1' and 'C1' don't virtually inherit from 'A'
Z1 z1;
// gets only 1 "copy" of 'A' base since 'B2' and 'C2' virtualy inherit from 'A' and thus "share" the vtable pointer to the 'A' base
Z2 z2;
z1.write(); // ambiguous call to write (which one is it .. B1::write() (since B1 inherits from A) or A::write() ?)
z1.read(); // ambiguous call to read (which one is it .. C1::read() (since C1 inherits from A) or A::read() ?)
z2.write(); // not ambiguous: z2.write() calls C2::write() since it's "virtually mapped" to/from A::write()
z2.read(); // not ambiguous: z2.read() calls B2::read() since it's "virtually mapped" to/from A::read()
return 0;
}
While it might be "obvious" to us humans which call we intend to make in the case of the z1
variable, since B1
doesn't have a write
method, I would "expect" the compiler to choose the C1::write
method, but due to how the memory mapping of objects work, it presents a problem since the base copy of A
in the C1
object might have different information (pointers/references/handles) than the copy of the A
base in the B1
object (since there's technically 2 copies of the A
base); thus a call to B1::read() { this->write(); }
could give unexpected behaviour (though not undefined).
The virtual
keyword on a base class specifier makes it explicit that other classes that virtually inherit from the same base type, shall only get 1 copy of the base type.
Note that the above code should fail to compile with compiler errors explaining the ambiguous calls for the z1
object. If you comment out the z1.write();
and z1.read();
lines the output (for me at least) is the following:
A::A
B1::B1
A::A
C1::C1
Z1::Z1
A::A
B2::B2
C2::C2
Z2::Z2
C2::write
B2::read
Note the 2 calls to the A
ctor (A::A
) before Z1
is constructed, while Z2
only has 1 call to the A
constructor.
I recommend reading the following on virtual inheritance as it goes more in depth on some of the other pitfalls to take note of (like the fact that virtually inherited classes need to use the initialization list to make base class ctor calls, or that you should avoid using C-style casts when doing such a type of inheritance).
It also explains a little more to what you were initially alluding to with the constructor/destructor ordering, and more specifically how the ordering is done when using multiple virtual inheritance.
Hope that can help clear things up a bit.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With