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.
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:
<code class="language-cpp"><span style="color: rgb(198, 120, 221)">class</span> <span style="color: rgb(230, 192, 123)">Base1</span> {<br/><span style="color: rgb(198, 120, 221)">public</span>:<br/> <span style="color: rgb(209, 154, 102)">virtual void</span> <span style="color: rgb(97, 174, 238)">fun</span>() {}<br/> <span style="color: rgb(198, 120, 221)">virtual</span> <span style="color: rgb(209, 154, 102)">void</span> <span style="color: rgb(97, 174, 238)">f1</span>() {}<br/> <span style="color: rgb(209, 154, 102)">int</span> a;<br/>};<br/><br/><span style="color: rgb(198, 120, 221)">class</span> <span style="color: rgb(230, 192, 123)">Derived</span> : <span style="color: rgb(198, 120, 221)">public</span> Base {<br/><span style="color: rgb(198, 120, 221)">public</span>:<br/> <span style="color: rgb(209, 154, 102)">void</span> <span style="color: rgb(97, 174, 238)">fun</span>() {} <span style="color: rgb(92, 99, 112); font-style: italic">// override Base::fun()</span><br/> <span style="color: rgb(209, 154, 102)">int</span> b;<br/>};<br/><br/><span style="color: rgb(209, 154, 102)">void</span> <span style="color: rgb(97, 174, 238)">call</span>(Base *b) {<br/> b-><span style="color: rgb(230, 192, 123)">fun</span>();<br/>}</code>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, ...}<br/>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.
<code class="language-cpp"><span style="color: rgb(198, 120, 221)">class</span> <span style="color: rgb(230, 192, 123)">Base1</span> {<br/><span style="color: rgb(198, 120, 221)">public</span>:<br/> <span style="color: rgb(209, 154, 102)">void</span> <span style="color: rgb(97, 174, 238)">f0</span>() {}<br/> <span style="color: rgb(198, 120, 221)">virtual</span> <span style="color: rgb(209, 154, 102)">void</span> <span style="color: rgb(97, 174, 238)">f1</span>() {}<br/> <span style="color: rgb(209, 154, 102)">int</span> a;<br/>};<br/><br/><span style="color: rgb(198, 120, 221)">class</span> <span style="color: rgb(230, 192, 123)">Base2</span> {<br/><span style="color: rgb(198, 120, 221)">public</span>:<br/> <span style="color: rgb(198, 120, 221)">virtual</span> <span style="color: rgb(209, 154, 102)">void</span> <span style="color: rgb(97, 174, 238)">f2</span>() {}<br/> <span style="color: rgb(209, 154, 102)">int</span> b;<br/>};<br/><br/><span style="color: rgb(198, 120, 221)">class</span> <span style="color: rgb(230, 192, 123)">Derived</span> : <span style="color: rgb(198, 120, 221)">public</span> Base1, <span style="color: rgb(198, 120, 221)">public</span> Base2 {<br/><span style="color: rgb(198, 120, 221)">public</span>:<br/> <span style="color: rgb(209, 154, 102)">void</span> <span style="color: rgb(97, 174, 238)">d</span>() {}<br/> <span style="color: rgb(209, 154, 102)">void</span> <span style="color: rgb(97, 174, 238)">f2</span>() {} <span style="color: rgb(92, 99, 112); font-style: italic">// override Base2::f1()</span><br/> <span style="color: rgb(209, 154, 102)">int</span> c;<br/>};<br/><br/><span style="color: rgb(209, 154, 102)">int</span> <span style="color: rgb(97, 174, 238)">main</span>() {<br/> Base2 *b2 = <span style="color: rgb(198, 120, 221)">new</span> Base2;<br/> Derived *d = <span style="color: rgb(198, 120, 221)">new</span> Derived;<br/>}</code>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<br/>Base2::_ZTV5Base2: 3u entries<br/>0 (int (*)(...))0<br/>8 (int (*)(...))(& _ZTI5Base2)<br/>16 (int (*)(...))Base2::f2<br/><br/>Class Base2<br/> size=16 align=8<br/> base size=12 base align=8<br/>Base2 (0x0x7ff572e6b600) 0<br/> 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<br/>$2 = 0x612c20<br/>(gdb) x/2xg 0x612c20<br/>0x612c20: 0x0000000000400918 0x0000000000000000<br/>(gdb) p &(((Base2*)0)->b)<br/>$3 = (int *) 0x8This 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<br/>Derived::_ZTV7Derived: 7u entries<br/>0 (int (*)(...))0<br/>8 (int (*)(...))(& _ZTI7Derived)<br/>16 (int (*)(...))Base1::f1<br/>24 (int (*)(...))Derived::f2<br/>32 (int (*)(...))-16<br/>40 (int (*)(...))(& _ZTI7Derived)<br/>48 (int (*)(...))Derived::_ZThn16_N7Derived2f2EvThe 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<br/>0x400763 <_ZThn16_N7Derived2f2Ev>: sub $0x10,%rdi<br/>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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
IT Services Circle
Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
