Virtual Inheritance

Difference between virtual and simple inheritance in C++

Assuming that there is no additional inheritance hierarchy, there is no difference in this case. The only way to see a difference is inheriting the same class through multiple ways - for example, as follows:

class ZA : public Z, public A {};

vs.

class ZA : virtual public Z, virtual public A {};

In the first case, ZA would have two regions with separate As - one inherited directly, and one inherited through Z. In the second case, there would be only one A, inherited through both paths, and shared.

Here is an illustration of this:

Virtual vs. Regular inheritance

C++ Virtual inheritance and constructors

There are two big keys here:

  1. Virtual base classes are initialized directly by the most derived class constructor, and not indirectly by other base classes.

  2. Virtual base classes are initialized before any non-virtual base classes.

So let's look at your examples.

When setting only B:A to virtual I get:

A() B(2) A(3) C(3) D()

That makes some sense - B inherits A virtually, and therefore, unless specifically stated otherwise, B(int x) calls A's default constructor.

B does not call A's constructor at all. D calls A directly for the virtual instance of A. But since the constructor of D doesn't specify how to initialize A, you get A's default constructor.

The A in B(int i) : A(i) gets ignored not because A is a virtual base, but because B is not the most derived class and so doesn't get to construct its A.

When setting only C:A to virtual I get:

A() A(2) B(2) C(3) D()

Why do A's constructors both precede B's and C's? That behavior makes no sense to me.

Here C::A is the only virtual base class, so it gets initialized first. Again, since the constructor of D doesn't specify an initializer for A, that virtual subobject uses the default constructor.

Then the non-virtual base classes go in order. B comes before C, but B first needs to initialize its non-virtual A (with 2).

When setting only D:C to virtual I get:

A(3) C(3) A(2) B(2) D()

Why would C's constructor precede B's?

The only virtual base class is C, so it gets constructed before B. But this time the constructor of C must first initialize its non-virtual base subobject C::A. Then comes B, which must first initialize its non-virtual subobject B::A.

Finally, there's the "normal" pattern, which makes both A inheritances virtual, resulting in only one A subobject, which is the entire point of virtual inheritance. It's pointless to make B or C virtual unless you expect D to be reused as a base class in an even more complicated way.

#include <iostream>
using namespace std;

struct A{
A() { cout << "A()" << endl; }
A(int i) { cout << "A(" << i << ")" << endl; }
};

struct B : virtual public A{
B(int i) : A(i){ cout << "B(" << i << ")" << endl; }
};

struct C : virtual public A{
C(int i) : A(i){ cout << "C(" << i << ")" << endl; }
};

struct D : public B, public C{
D() : A(1), B(2), C(3){ cout << "D()" << endl; }
};

void main() {
D d;
system("pause");
}

Output:

A(1) B(2) C(3) D()

Note I've added an initializer A(1) to D, to show that a virtual subobject does not necessarily need to use the default constructor. You could have done this in any of your examples where at least one A inheritance is virtual.

Why does virtual inheritance need a vtable even if no virtual functions are involved?

The complete class inheritance hierarchy is already known in compile time.

True enough; so if the compiler knows the type of a most derived object, then it knows the offset of every subobject within that object. For such a purpose, a vtable is not needed.

For example, if B and C both virtually derive from A, and D derives from both B and C, then in the following code:

D d;
A* a = &d;

the conversion from D* to A* is, at most, adding a static offset to the address.

However, now consider this situation:

A* f(B* b) { return b; }
A* g(C* c) { return c; }

Here, f must be able to accept a pointer to any B object, including a B object that may be a subobject of a D object or of some other most derived class object. When compiling f, the compiler doesn't know the full set of derived classes of B.

If the B object is a most derived object, then the A subobject will be located at a certain offset. But what if the B object is part of a D object? The D object only contains one A object and it can't be located at its usual offsets from both the B and C subobjects. So the compiler has to pick a location for the A subobject of D, and then it has to provide a mechanism so that some code with a B* or C* can find out where the A subobject is. This depends solely on the inheritance hierarchy of the most derived type---so a vptr/vtable is an appropriate mechanism.

Virtual inheritance impact on constructor

Firstly, since B derives from A non-virtually, D ends up with two A subobjects (as if you didn't use virtual inheritance at all).

The non-virtual A is constructed by B() (hence "A called from B"), and the virtual one prints "Constructor A" when constructed.

That's because the constructors of virtual (possibly indirect) bases are always called by the constructor of the most derived class (D), rather than by the constructors of any intermediate bases (C).

It means that : A("A called from C") in C() is ignored (since you're not constructing a standalone C). And since D() doesn't mention A in its member initializer list, the virtual A is constructed without any parameters (as if by : A()).


Also, it's worth mentioning that all virtual bases are constructed before the non-virtual ones.

So your code is guaranteed to print Constructor A before A called from B.

Mixing virtual and non-virtual inheritance of a base class

It's clear from the output that two Biology objects are instantiated. That is because you've made only one inheritance virtual. Two base class instances is the cause of ambiguity in dreaded diamond problem and the solution is to make (as we know) both inheritances of Biology virtual.

Recap of the hierarchy:

Biology  Biology
| | # one and only one inheritance virtual
Human Animal
\ /
Centaur

Ok, let's read the output again with these rules in mind:

  • Base classes are constructed before derived classes.
  • Base classes are constructed in order in which they appear in the base-specifier-list.
  • Virtual base classes are constructed before non-virtual ones by the most derived class - see this.

1st output - Animal virtually inherits from Biology:

Biology CTOR     # virtual base class inherited from Animal
Biology CTOR # non-virtual base class of Human
Human CTOR # Human itself
Animal CTOR # Animal's virtual base class already constructed
Centaur CTOR

2nd output - Human virtually inherits from Biology:

Biology CTOR     # virtual base class inherited from Human
Human CTOR # Human's virtual base class already constructed
Biology CTOR # non-virtual base class of Animal
Animal CTOR # Animal itself
Centaur CTOR

More informative standard paragraph ([class.base.init]/10):

In a non-delegating constructor, initialization proceeds in the
following order:

— First, and only for the constructor of the most
derived class (1.8), virtual base classes are initialized in the order
they appear on a depth-first left-to-right traversal of the directed
acyclic graph of base classes, where “left-to-right” is the order of
appearance of the base classes in the derived class
base-specifier-list.

— Then, direct base classes are initialized in
declaration order as they appear in the base-specifier-list
(regardless of the order of the mem-initializers).

...

How does virtual inheritance actually work?

With a single level of inheritance, there's no difference in behaviour.

The difference is when inheriting from multiple base classes, which themselves have a common base class:

struct A {};
struct B : virtual A {};
struct C : virtual A {};
struct D : B,C {};

In this example, with virtual inheritance, D contains just one A sub-object, which its B and C subobjects share; this is the "diamond" pattern:

    A
/ \
B C
\ /
D

Without virtual inheritance, it would contain two A sub-objects, and no "diamond":

   A A
| |
B C
\ /
D

With or without multiple inheritance, there's still a difference if you have more than one level of inheritance: a virtual base class must be initialised by the most derived class, not by its immediate derived class. This is to avoid the ambiguity in the case of multiple inheritance, where (in the example above) both B and C would otherwise be responsible for initialising the shared A.

Size of the classes in case of virtual inheritance

sizeof(A): 8

3 bytes in the array, 1 byte padding, 4 bytes for the vptr (pointer to the vtable)

sizeof(B): 12

A subobject: 8, 3 bytes for the extra array, 1 byte padding

sizeof(C): 16

This is probably the surprising one for you...
A subobject: 8, 3 bytes for the extra array, 1 byte padding, 4 bytes pointer to A

Whenever you have virtual inheritance, the location of the virtual base subobject with respect to the start of the complete type is unknown, so an extra pointer is added to the original object to track where the virtual base is. Consider this example:

struct A {};
struct B : virtual A {};
struct C : virtual A {};
struct D : B, C {};

The location of A with respect to the start of the B object when the complete type is a B can be different than the location of the A subobject of B when it is part of the final object D. If this is not obvious, assume that the relative location is the same, and check whether the relative location of A with respect to C in a C final object or a C subobject in D can also be maintained.

As for the last example, I don't quite feel like analyzing it... but you can read the Itanium C++ ABI for a concrete implementation of the C++ object model. All other implementations don't differ much.


Last example:

sizeof(D): 32

D contains a B subobject (12), and a C subobject (16), plus an additional array of size 3 and one extra bit of padding 1.

In this particular case, the question that could come up is why are there two A subobjects if C inherits virtually from A, and the answer is that virtual base means that the object is willing to share this base with any other type in the hierarchy that is also willing to share it. But in this case B is not willing to share it's A subobject, so C needs it's own.

You should be able to track this by adding logs to the constructors at the different levels. In the case of A have it take a value in the compiler and pass different values from each extending class.



Related Topics



Leave a reply



Submit