C++ Virtual Table Table
Introduction
In my previous article “C++ Virtual Table”, we have discussed what’s vtable and how is vtable used for runtime polymorphisms in the context of non-virtual inheritance and virtual inheritance in C++.
There is a special concept called virtual table table, i.e., vtable table, which we did not discuss. The vtable table are static array of vtable pointers pointing to the vtables used for object construction and destruction. It only exists in the context of virtual inheritance. Some of the vtables that vtable table points to are called construction vtables, and they are only used for object construction and destruction.
In this blog post, I would like to discuss the C++ vtable table in detail and show a couple of examples.
Virtual Table for Construction and Destruction
Although some programmers are not aware of it, C++ vtables are needed for object construction and destructions as well.
Let’s look at a quick example, even if it just uses non-virtual inheritance.
1 |
|
The program will construct a C
class object and destruct the C
class object. The C
class inherits the B
class and the B
class inherits the A
class. The A
class have three virtual functions, foo()
, bar()
, and baz
. They are also overridden in both the B
class and the C
class. In addition, the virtual functions foo()
and bar()
will be called in the constructors and the destructors for each class, respectively.
1 | $ g++ vtable_construction_destruction.cpp -o vtable_construction_destruction |
The output looks quite normal as expected. To construct a C
class object, we will first have to construct a A
class object followed by a B
class object. When the virtual function baz
is called via a B
class pointer pointing to a C
class object, the baz
function overridden in C, C::baz()
, will be called because of the runtime polymorphisms thanks to the vtable.
Note that during the constructor and destruction of each subobject, when foo()
and bar()
are called, the overriding functions in the most derived class cannot be called. For example, during the construction of the A
class subobjct for a C
class object, A::foo()
was called instead of C::foo()
. This is because when the A
class subobject is being constructed, the C
class object has not been constructed and calling the overriding function in the C
class, which might modify the C
class that has not been constructed, will result in undefined behaviors. We have three vtables related to the A
class in this example, an A
class vtable, a B
class vtable which contains an A
base class vtable, and a C
class vtable which also contains an A
base class vtable. During the construction of the A
class subobject for a C
class object, the A
class vtable must be used. Otherwise, if the B
class vtable which contains an A
base class vtable or the C
class vtable which also contains an A
base class vtable are used, because of the function overriding, A::foo()
would not have been called.
This example tells us that there can be multiple vtables used during an object construction and destruction. Those vtables are called construction (and destruction) vtables. To construct or destruct an object correctly, the right construction vtables have to be used.
Virtual Table Table for Construction Virtual Table Management
Because there can be many construction vtables during the construction and destruction of a class object, a vtable table can be created for dispatching the correct construction vtable.
In principle, vtable table can be used for both non-virtual inheritance and virtual inheritance. In practice, however, vtable table is only used for virtual inheritance. We will discuss these with concrete examples next.
Non-Virtual Inheritance
If a class has no virtual inheritance, during the class object construction and destruction, the base class vtables can just be used.
Let’s use the non-virtual inheritance example from my previous article “C++ Virtual Table”, we have the following inheritance diagram.
1 | Fruit |
The vtables for the Fruit
class and the Apple
class are demonstrated below.
1 | Vtable for Fruit |
1 | Vtable for Apple |
To construct an Apple
class object, we will construct a Fruit
class subobject first. The Fruit
class vtable Fruit::_ZTV5Fruit
can just be reused. The Fruit
class subobject vtable pointer will just point to the Fruit
class vtable Fruit::_ZTV5Fruit
and use the virtual functions there if necessary. Once the Fruit
class subobject construction is complete, the Apple
class object vtable pointer, which is at the same memory address as the Fruit
class subobject vtable pointer, will point to the Apple
class vtable Apple::_ZTV5Apple
and use the virtual functions there if necessary.
Everything seems to be very straightforward. If all the inheritances in C++ are non-virtual, nobody will come up with an idea of vtable table as it’s not needed.
Virtual Inheritance
If a class has virtual inheritance, during the class object construction and destruction, the base class vtables cannot always be used.
Let’s use the virtual inheritance example from my previous article “C++ Virtual Table”, we have the following inheritance diagram.
1 | Item |
The vtables for the Item
class, the Fruit
class and the Apple
class are demonstrated below.
1 | Vtable for Item |
1 | Vtable for Fruit |
1 | Vtable for Apple |
To construct an Apple
class object, we will construct a Item
class virtual base subobject first. The Item
class vtable Item::_ZTV4Item
can just be reused. Not a problem.
However, when it comes to constructing a Fruit
class subobject, we got a problem that the Fruit
class vtable Fruit::_ZTV5Fruit
cannot be reused. Suppose we insist using the Fruit
class vtable Fruit::_ZTV5Fruit
for the Fruit
class subobject construction, the Fruit
class subobject vtable pointer will point to offset 24
. This has no problem. If the Fruit
class constructor has to call the virtual function qux
declared in the virtual base Item
class. The virtual base Item
class subobject vtable pointer will have to be found first. Its offset from the Fruit
class subobject vtable pointer, which points to the vtable Fruit::_ZTV5Fruit
at offset 104
, can be queried from the vtable Fruit::_ZTV5Fruit
vbase-offset at offset 0
, which is 24
. Then Item::qux
will be invoked. The problem is, the Item
class subobject vtable pointer offset from the the Fruit
class subobject vtable pointer, which is 24
according to the vbase-offset we just obtained, is incorrect. The correct vbase-offset value, which we could peek from the Apple
class vtable Apple::_ZTV5Apple
, is 32
. Fundamentally, this is due to the vtable layout of the virtual inheritance vtable is different from the vtable layout of the non-virtual inheritance vtable.
To fix this problem, we will need a new vtable for the Fruit
class in the Apple
class, which is called the construction vtable for Fruit
in Apple
demonstrated below.
1 | Construction vtable for Fruit (0x0x7fa3f5c0e5b0 instance) in Apple |
Using this vtable, we can correctly construct or destruct the Fruit
class subobject inside an Apple
class subject. This vtable is only used for construction and destruction.
When the Fruit
class is inherited by multiple different classes, there will be multiple different construction vtable for Fruit
in different classes. When the number of such classes, the construction vtable management becomes complicated.
Because of this, the vtable table comes into play. For each class that has virtual inheritance, there is a vtable table so that the correct vtables can be dispatched during the class object construction and destruction.
In our example, the vtable table for the Apple
class is demonstrated below.
1 | VTT for Apple |
It consists of four vtable pointers that point to two vtables, including Apple::_ZTV5Apple
and Apple::_ZTC5Apple0_5Fruit
.
The summary of the Apple
class and vtable information are shown below.
1 | Class Apple |
Apple (0x0x7fa3f5c0e548) 0
says the Apple
class vtable pointer vptr
points to ((& Apple::_ZTV5Apple) + 24)
and its offset in the vtable table vptridx
is 0
. That is to say, vptr = *(& Apple::_ZTT5Apple + vptridx) = ((& Apple::_ZTV5Apple) + 24)
.
Fruit (0x0x7fa3f5c0e5b0) 0
says the Fruit
class is the primary base class for Apple
so that it shares the vtable pointer vptr
with the Apple
class. The construction vtable pointer offset in the vtable table subvttidx
is 8
, which means the Fruit
class in the Apple
class uses the vtable pointer *(& Apple::_ZTT5Apple + subvttidx) = ((& Apple::_ZTC5Apple0_5Fruit) + 24)
for construction and destruction. In fact, the first line in the construction vtable Construction vtable for Fruit (0x0x7fa3f5c0e5b0 instance) in Apple
already showed the instance ID for Fruit
in Apple
which is 0x0x7fa3f5c0e5b0
and it is consistent with the one in Fruit (0x0x7fa3f5c0e5b0) 0
.
Item (0x0x7fa3f5d6dde0) 32 virtual
says the virtual base Item
class vtable pointer vptr
points to ((& Apple::_ZTV5Apple) + 112)
and the pointer pointing to the vptr
offset in the vtable table vptridx
is 24
. That is to say, vptr = *(& Apple::_ZTT5Apple + vptridx) = ((& Apple::_ZTV5Apple) + 112)
. The vbaseoffset
in this case is a little bit confusing. It does not mean the Apple
class (vtable) pointer or the Fruit
class (vtable) pointer offset to the Item
class (vtable) pointer is -24
. In fact, they should be 32
as we could see from the Apple
class vtable Apple::_ZTV5Apple
. Instead, it actually means in the vtable of the class which virtually inherits the Item
class in the Apple
class, the offset between the vtable pointer vptr
and the vbase-offset entry is -24
. That’s to say, to find the vbase-offset value given a vtable pointer vptr
, the vbase-offset value is *(vptr - 24)
.
Conclusions
We have discussed the motivation of using vtable in virtual inheritance and how it is used for object construction and destruction. But notice that we have never discussed how a vtable or a vtable table is constructed by the compiler, we just used whatever vtable and vtable table constructed by the compiler in this article.
References
C++ Virtual Table Table