I read this question: C++ Virtual class inheritance object size issue, and was wondering why virtual inheritance results in an additional vtable pointer in the class.
I found an article here: https://en.wikipedia.org/wiki/Virtual_inheritance
which tells us:
However this offset can in the general case only be known at runtime,...
I don't get what is runtime-related here. The complete class inheritance hierarchy is already known at compile time. I understand virtual functions and the use of a base pointer, but there is no such thing with virtual inheritance.
Can someone explain why some compilers (Clang/GCC) implement virtual inheritance with a vtable and how this is used during runtime?
BTW, I also saw this question: vtable in case of virtual inheritance, but it only points to answers related to virtual functions, which is not my question.
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.
The complete class inheritance hierarchy is already known at compiler time. But all the vptr
related operations, such as to get the offsets to virtual base class and issue the virtual function call, are delayed until runtime, because only at runtime can we know the actual type of the object.
For example,
class A() { virtual bool a() { return false; } };
class B() : public virtual A { int a() { return 0; } };
B* ptr = new B();
// assuming function a()'s index is 2 at virtual function table
// the call
ptr->a();
// will be transformed by the compiler to (*ptr->vptr[2])(ptr)
// so a right call to a() will be issued according to the type of the object ptr points to
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.
+--------------+
| Animal |
+--------------+
| vpointer |
| Mammal |
+--------------+
| vpointer |
| WingedAnimal |
+--------------+
| vpointer |
| Bat |
+--------------+
+--------------+
| 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.
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