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.
However this offset can in the general case only be known at runtime,...
I can't get the point, what is runtime related here. The complete class inheritance hierarchy is already known in compile time.
The linked article at Wikipedia provides a good explanation with examples, I think.
The example code from that article:
struct Animal {
virtual ~Animal() = default;
virtual void Eat() {}
};
// Two classes virtually inheriting Animal:
struct Mammal : virtual Animal {
virtual void Breathe() {}
};
struct WingedAnimal : virtual Animal {
virtual void Flap() {}
};
// A bat is still a winged mammal
struct Bat : Mammal, WingedAnimal {
};
When you careate an object of type Bat
, there are various ways a compiler may choose the object layout.
Option 1
+--------------+
| Animal |
+--------------+
| vpointer |
| Mammal |
+--------------+
| vpointer |
| WingedAnimal |
+--------------+
| vpointer |
| Bat |
+--------------+
Option 2
+--------------+
| vpointer |
| Mammal |
+--------------+
| vpointer |
| WingedAnimal |
+--------------+
| vpointer |
| Bat |
+--------------+
| Animal |
+--------------+
The values contained in vpointer
in Mammal
and WingedAnimal
define the offsets to the Animal
sub-object. Those values cannot be known until run time because the constructor of Mammal
cannot know whether the subject is Bat
or some other object. If the sub-object is Monkey
, it won't derive from WingedAnimal
. It will be just
struct Monkey : Mammal {
};
in which case, the object layout could be:
+--------------+
| vpointer |
| Mammal |
+--------------+
| vpointer |
| Monkey |
+--------------+
| Animal |
+--------------+
As can be seen, the offset from the Mammal
sub-object to the Animal
sub-object is defined by the classes derived from Mammal
. Hence, it can be defined only at runtime.