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.

vtable_construction_destruction.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#include <iostream>
#include <memory>

class A
{
public:
A()
{
std::cout << "A::A() is called" << std::endl;
foo();
}
virtual ~A()
{
std::cout << "A::~A() is called" << std::endl;
bar();
}
virtual void foo() const { std::cout << "A::foo() is called" << std::endl; }
virtual void bar() const { std::cout << "A::bar() is called" << std::endl; }
virtual void baz() const { std::cout << "A::baz() is called" << std::endl; }
};

class B : public A
{
public:
B()
{
std::cout << "B::B() is called" << std::endl;
foo();
}
~B()
{
std::cout << "B::~B() is called" << std::endl;
bar();
}
virtual void foo() const override
{
std::cout << "B::foo() is called" << std::endl;
}
virtual void bar() const override
{
std::cout << "B::bar() is called" << std::endl;
}
virtual void baz() const override
{
std::cout << "B::baz() is called" << std::endl;
}
};

class C : public B
{
public:
C()
{
std::cout << "C::C() is called" << std::endl;
foo();
}
~C()
{
std::cout << "C::~C() is called" << std::endl;
bar();
}
void foo() const override
{
std::cout << "C::foo() is called" << std::endl;
}
void bar() const override
{
std::cout << "C::bar() is called" << std::endl;
}
virtual void baz() const override
{
std::cout << "C::baz() is called" << std::endl;
}
};

int main()
{
std::cout << "---------------------------------------------" << std::endl;
std::cout << "C Construction..." << std::endl;
std::cout << "---------------------------------------------" << std::endl;
C const* const ptr_c = new C{};
std::cout << "---------------------------------------------" << std::endl;
std::cout << "C Construction Complete." << std::endl;
std::cout << "---------------------------------------------" << std::endl;
B const* const ptr_b = dynamic_cast<B const* const>(ptr_c);
std::cout << "Calling baz From Base B Class Pointer..." << std::endl;
std::cout << "---------------------------------------------" << std::endl;
ptr_b->baz();
std::cout << "---------------------------------------------" << std::endl;
std::cout << "C Destruction..." << std::endl;
std::cout << "---------------------------------------------" << std::endl;
delete ptr_c;
std::cout << "---------------------------------------------" << std::endl;
std::cout << "C Destruction complete." << std::endl;
std::cout << "---------------------------------------------" << std::endl;
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$ g++ vtable_construction_destruction.cpp -o vtable_construction_destruction
$ ./vtable_construction_destruction
---------------------------------------------
C Construction...
---------------------------------------------
A::A() is called
A::foo() is called
B::B() is called
B::foo() is called
C::C() is called
C::foo() is called
---------------------------------------------
C Construction Complete.
---------------------------------------------
Calling baz From Base B Class Pointer...
---------------------------------------------
C::baz() is called
---------------------------------------------
C Destruction...
---------------------------------------------
C::~C() is called
C::bar() is called
B::~B() is called
B::bar() is called
A::~A() is called
A::bar() is called
---------------------------------------------
C Destruction complete.
---------------------------------------------

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
2
3
Fruit
|
Apple

The vtables for the Fruit class and the Apple class are demonstrated below.

1
2
3
4
5
6
7
8
Vtable for Fruit
Fruit::_ZTV5Fruit: 6 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI5Fruit)
16 0
24 0
32 (int (*)(...))__cxa_pure_virtual
40 (int (*)(...))Fruit::bar
1
2
3
4
5
6
7
8
9
Vtable for Apple
Apple::_ZTV5Apple: 7 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI5Apple)
16 (int (*)(...))Apple::~Apple
24 (int (*)(...))Apple::~Apple
32 (int (*)(...))Apple::foo
40 (int (*)(...))Fruit::bar
48 (int (*)(...))Apple::apple_foo

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
2
3
4
5
Item
|
Fruit
|
Apple

The vtables for the Item class, the Fruit class and the Apple class are demonstrated below.

1
2
3
4
5
6
7
8
Vtable for Item
Item::_ZTV4Item: 6 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI4Item)
16 (int (*)(...))Item::~Item
24 (int (*)(...))Item::~Item
32 (int (*)(...))Item::qux
40 (int (*)(...))Item::quux
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Vtable for Fruit
Fruit::_ZTV5Fruit: 17 entries
0 24
8 (int (*)(...))0
16 (int (*)(...))(& _ZTI5Fruit)
24 0
32 0
40 (int (*)(...))__cxa_pure_virtual
48 (int (*)(...))Fruit::bar
56 (int (*)(...))Fruit::quux
64 18446744073709551592
72 0
80 18446744073709551592
88 (int (*)(...))-24
96 (int (*)(...))(& _ZTI5Fruit)
104 0
112 0
120 (int (*)(...))Item::qux
128 (int (*)(...))Fruit::_ZTv0_n40_N5Fruit4quuxEv
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Vtable for Apple
Apple::_ZTV5Apple: 18 entries
0 32
8 (int (*)(...))0
16 (int (*)(...))(& _ZTI5Apple)
24 (int (*)(...))Apple::~Apple
32 (int (*)(...))Apple::~Apple
40 (int (*)(...))Apple::foo
48 (int (*)(...))Fruit::bar
56 (int (*)(...))Fruit::quux
64 (int (*)(...))Apple::apple_foo
72 18446744073709551584
80 0
88 18446744073709551584
96 (int (*)(...))-32
104 (int (*)(...))(& _ZTI5Apple)
112 (int (*)(...))Apple::_ZTv0_n24_N5AppleD1Ev
120 (int (*)(...))Apple::_ZTv0_n24_N5AppleD0Ev
128 (int (*)(...))Item::qux
136 (int (*)(...))Fruit::_ZTv0_n40_N5Fruit4quuxEv

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Construction vtable for Fruit (0x0x7fa3f5c0e5b0 instance) in Apple
Apple::_ZTC5Apple0_5Fruit: 17 entries
0 32
8 (int (*)(...))0
16 (int (*)(...))(& _ZTI5Fruit)
24 0
32 0
40 (int (*)(...))__cxa_pure_virtual
48 (int (*)(...))Fruit::bar
56 (int (*)(...))Fruit::quux
64 18446744073709551584
72 0
80 18446744073709551584
88 (int (*)(...))-32
96 (int (*)(...))(& _ZTI5Fruit)
104 0
112 0
120 (int (*)(...))Item::qux
128 (int (*)(...))Fruit::_ZTv0_n40_N5Fruit4quuxEv

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
2
3
4
5
6
VTT for Apple
Apple::_ZTT5Apple: 4 entries
0 ((& Apple::_ZTV5Apple) + 24)
8 ((& Apple::_ZTC5Apple0_5Fruit) + 24)
16 ((& Apple::_ZTC5Apple0_5Fruit) + 104)
24 ((& Apple::_ZTV5Apple) + 112)

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
2
3
4
5
6
7
8
9
10
Class Apple
size=48 align=8
base size=28 base align=8
Apple (0x0x7fa3f5c0e548) 0
vptridx=0 vptr=((& Apple::_ZTV5Apple) + 24)
Fruit (0x0x7fa3f5c0e5b0) 0
primary-for Apple (0x0x7fa3f5c0e548)
subvttidx=8
Item (0x0x7fa3f5d6dde0) 32 virtual
vptridx=24 vbaseoffset=-24 vptr=((& Apple::_ZTV5Apple) + 112)

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

Author

Lei Mao

Posted on

07-24-2023

Updated on

07-24-2023

Licensed under


Comments