Fundamentals 20 min read

Understanding C++ Polymorphism: Vtable Layout, Multiple Inheritance, and Thunks

This article explains how C++ implements runtime polymorphism through virtual function tables, analyzes the memory layout of single‑ and multiple‑inheritance classes using gcc and gdb, and clarifies the role of thunks and offset adjustments for correct virtual calls.

IT Services Circle
IT Services Circle
IT Services Circle
Understanding C++ Polymorphism: Vtable Layout, Multiple Inheritance, and Thunks

In the previous article C++: A Technical Look at RTTI , we discussed the virtual function table (vtable) and part of its layout. To deepen the understanding of C++ object memory layout, this article analyses the vtable and related structures using gcc and gdb.

Polymorphism

It is well known that C++ implements runtime polymorphism via virtual functions, whose underlying mechanism is the virtual function table (vtable). This knowledge is concise but essential, often appearing in interview questions.

Using the code from the previous article as an example:

class
Base1
{
public
:
virtual void
fun
() {}
virtual
void
f1
() {}
int
a;
};
class
Derived
:
public
Base {
public
:
void
fun
() {}
// override Base::fun()
int
b;
};
void
call
(Base *b) {
b->
fun
();
}

When b points to a Base object, call() invokes Base::fun() ; when it points to a Derived object, it invokes Derived::fun() . This works because each class that has virtual functions owns a vtable, and each object contains a hidden pointer ( vptr ) to its class’s vtable.

For every class with virtual functions there is a table that stores pointers to those functions (and other entries).

vtable_Base = {&Base::func, ...}
vtable_Derived = {&Derived::func, ...}

When an object is created, the first field is a pointer ( vptr ) that points to the appropriate entry in the class’s vtable. (Note: the pointer does not point to the very beginning of the vtable.)

Therefore, the call b->fun() is essentially performed as ((Vtable*)b)[0]() , where the index is determined by the declaration order of the virtual functions.

Implementation

The following example uses multiple inheritance to illustrate the layout.

class
Base1
{
public
:
void
f0
() {}
virtual
void
f1
() {}
int
a;
};
class
Base2
{
public
:
virtual
void
f2
() {}
int
b;
};
class
Derived
:
public
Base1,
public
Base2 {
public
:
void
d
() {}
void
f2
() {}
// override Base2::f1()
int
c;
};
int
main
() {
Base2 *b2 =
new
Base2;
Derived *d =
new
Derived;
}

We first compile with -fdump-class-hierarchy to obtain a high‑level view, then use gdb for detailed inspection.

Base class

The dump for Base2 shows its vtable and size:

Vtable for Base2
Base2::_ZTV5Base2: 3u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI5Base2)
16    (int (*)(...))Base2::f2
Class Base2
size=16 align=8
base size=12 base align=8
Base2 (0x0x7ff572e6b600) 0
vptr=((& Base2::_ZTV5Base2) + 16u)

The mangled name _ZTV5Base2 demangles to “vtable for Base2”. The first entry is an offset (0), the second entry points to RTTI information ( _ZTI5Base2 ), and the third entry is the actual function pointer for Base2::f2 .

Using gdb we can verify the layout:

(gdb) p/x $rax
$2 = 0x612c20
(gdb) x/2xg 0x612c20
0x612c20: 0x0000000000400918 0x0000000000000000
(gdb) p &(((Base2*)0)->b)
$3 = (int *) 0x8

This shows that the member b resides at offset 8, i.e., immediately after the vptr.

Multiple inheritance

Compiling the Derived class yields the following vtable dump:

Vtable for Derived
Derived::_ZTV7Derived: 7u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI7Derived)
16    (int (*)(...))Base1::f1
24    (int (*)(...))Derived::f2
32    (int (*)(...))-16
40    (int (*)(...))(& _ZTI7Derived)
48    (int (*)(...))Derived::_ZThn16_N7Derived2f2Ev

The first part (entries 0‑24) corresponds to the primary base Base1 . The entry with value -16 is an offset that adjusts the this pointer when a call originates from the Base2 sub‑object. The last entry is a non‑virtual thunk that adds the required offset before invoking Derived::f2 .

GDB confirms the thunk implementation:

(gdb) x/2i 0x0000000000400763
0x400763 <_ZThn16_N7Derived2f2Ev>: sub    $0x10,%rdi
0x400767 <_ZThn16_N7Derived2f2Ev+4>: jmp    0x400758 <_ZN7Derived2f2Ev>

Thus the compiler generates a small wrapper that subtracts the size of Base1 from the this pointer and then calls the real function, eliminating the need for a runtime pointer adjustment.

Offset (top‑offset)

The “offset to top” stored in the vtable tells the runtime how far the sub‑object’s address is from the actual object’s start. For Derived , the offset for the Base2 sub‑object is -16 , meaning the Base2 pointer must be moved 16 bytes backward to reach the true object address before invoking its virtual functions.

Conclusion

The article demonstrates that in multiple inheritance each base class keeps its own vptr because their virtual function tables are independent; they cannot be merged into a single table. Understanding vtables, RTTI entries, thunks, and top‑offsets is crucial for low‑level debugging and for answering interview questions about C++ object layout.

C++memory layoutGDBpolymorphismMultiple Inheritancevtablethunk
IT Services Circle
Written by

IT Services Circle

Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.