How Virtual Tables Work in the Itanium C++ ABI

Lobsters Hottest News

Summary

A detailed blog post explaining how virtual tables (vtables) are implemented in the Itanium C++ ABI, covering vtable structure, mangled names, and virtual function dispatch.

<p><a href="https://lobste.rs/s/kzslib/how_virtual_tables_work_itanium_c_abi">Comments</a></p>
Original Article
View Cached Full Text

Cached at: 05/26/26, 01:20 PM

# How Virtual Tables Work in the Itanium C++ ABI Source: [https://peter0x44.github.io/posts/vtables-itanium-abi/](https://peter0x44.github.io/posts/vtables-itanium-abi/) Virtual functions are one of C\+\+’s core features, enabling runtime polymorphism\. Most C\+\+ programmers use them regularly, yet few understand how they work in practice\. What does the compiler actually generate when you declare a function`virtual`? How does the program figure out which implementation to call at runtime? Where is the vtable data actually stored? This blog post is focused on answering these questions\. The C\+\+ standard specifies behavior but not implementation\. This post describes the[Itanium C\+\+ ABI](https://itanium-cxx-abi.github.io/cxx-abi/abi.html)used by most platforms, with the notable exception of Microsoft MSVC\. ## A Basic Example Consider these classes with virtual functions: ``` struct Base { virtual void foo() { __builtin_printf("Base::foo\n"); } virtual void bar() { __builtin_printf("Base::bar\n"); } virtual ~Base() {} }; struct Derived : Base { void foo() override { __builtin_printf("Derived::foo\n"); } }; void call_foo(Base* b) { b->foo(); // Which foo() gets called? } int main() { Base base; Derived derived; call_foo(&base); // Should call Base::foo call_foo(&derived); // Should call Derived::foo } ``` The compiler doesn’t know at compile time what type`b`points to in`call\_foo\(\)`\. The function needs to dispatch to the correct`foo\(\)`implementation based on the actual runtime type of the object\. This is what vtables \(short for virtual tables\) enable\. ## The Virtual Table Structure Let’s see what a vtable actually looks like\. GCC has a useful flag`\-fdump\-lang\-class`that dumps the class and vtable layout: ``` g++ -fdump-lang-class example.cpp ``` GCC should emit a file named`a\-example\.cpp\.001l\.class`containing info about the classes and vtables dumps\. Clang can dump similar information too, although the interface is different: ``` clang++ -Xclang -fdump-record-layouts -Xclang -fdump-vtable-layouts example.cpp ``` I learned about this from[Dumping a C\+\+ object’s memory layout with Clang](https://eli.thegreenplace.net/2012/12/17/dumping-a-c-objects-memory-layout-with-clang)\. I think GCC has a slightly nicer output format, so all quoted output below is from`gcc \-fdump\-lang\-class`\. For our`Base`class, GCC outputs: ``` Vtable for Base Base::_ZTV4Base: 6 entries 0 (int (*)(...))0 8 (int (*)(...))(& _ZTI4Base) 16 (int (*)(...))Base::foo 24 (int (*)(...))Base::bar 32 (int (*)(...))Base::~Base 40 (int (*)(...))Base::~Base Class Base size=8 align=8 base size=8 base align=8 Base (0x0x23116c0) 0 vptr=((& Base::_ZTV4Base) + 16) ``` Let’s break down what we’re seeing here\. ### Mangled Names Before looking at the entries, notice the symbol name:`\_ZTV4Base`\. This is the**mangled name**for the vtable\. - `\_Z`prefix indicates Itanium name mangling - `T`is a special name prefix \(used for vtables, typeinfo, thunks, and other compiler\-generated symbols\) - `V`means vtable \(`I`is typeinfo\) - `4`is the length of the following name - `Base`is the class name Similarly: - `\_ZTV7Derived`= vtable for Derived - `\_ZTI4Base`= typeinfo for Base ### Vtable Layout The vtable named`\_ZTV4Base`has 6 entries, each 8 bytes apart \(on a 64\-bit system\)\. The structure is: ``` Offset 0: offset-to-top = 0 Offset 8: typeinfo pointer = &_ZTI4Base Offset 16: foo() = &Base::foo Offset 24: bar() = &Base::bar Offset 32: ~Base() = &Base::~Base (D1)(complete object destructor) Offset 40: ~Base() = &Base::~Base (D0)(deleting destructor) ``` ### Virtual Function Pointers Each virtual function declared in the class gets an entry in the vtable, in declaration order\. Functions declared in`Base`are`foo\(\)`,`bar\(\)`, and the destructor\. Destructors get two vtable entries in the Itanium ABI: - **Complete object destructor \(D1\)**\(offset 32\): Destroys the object but doesn’t free memory\. Used for stack objects, members, and array elements\. - **Deleting destructor \(D0\)**\(offset 40\): Calls the complete destructor \(D1\), then calls`operator delete`to free memory\. Used when you write`delete ptr`\. There’s also a third variant, the**base object destructor \(D2\)**, which doesn’t appear in vtables and is only called directly by derived destructors\. We’ll see why it exists when we look at virtual inheritance\. ### Offset\-to\-Top This field records how far the vptr sits from the start of the complete object\. In single inheritance, its value is zero, so it looks uninteresting\. It becomes important once multiple inheritance is involved\. ### RTTI Typeinfo Pointer The typeinfo pointer enables runtime type identification \(RTTI\)\. It points to a`std::type\_info`object for the class, which is used by`dynamic\_cast`,`typeid`, and exception handling\. If`\-fno\-rtti`is used, this field is null\. ## Where Do Vtables Live? Vtables are static data structures that live in the`\.rodata`section of the binary\. But which translation unit actually emits them? The Itanium ABI uses the**key function rule**: the vtable is emitted in the translation unit that defines the first non\-inline virtual function\. ``` // base.h struct Base { virtual void foo() { /* ... */ }; // Inline, not the key function virtual void bar(); // Not inline - this is the key function virtual ~Base(); }; // base.cpp void Base::bar() { /* ... */ } // key function - vtable will be in this TU // Vtable for Base is emitted in base.cpp ``` This rule is the source of a common linker error: ``` undefined reference to `vtable for Base' ``` This happens when the key function is declared but not defined anywhere\. If all virtual functions are inline, there’s no key function\. The vtable is instead emitted with weak linkage in every translation unit that uses the class\. ## Object Layout: The VPtr Each object that has virtual functions contains a hidden member called the**virtual table pointer**\(vptr for short\)\. In this simple case using only single\-inheritance, it sits at offset 0 of the object\. Virtual dispatch reads`\*\(void\*\*\)this`to find the vtable\. With multiple inheritance there are several vptrs \(one per base class\)\. I will cover that later\. ``` Class Base size=8 align=8 base size=8 base align=8 Base (0x0x22606c0) 0 nearly-empty vptr=((& Base::_ZTV4Base) + 16) ``` Note that the vptr doesn’t point to the start of the vtable\. Instead, it points to the vtable’s**address point**\. In this case, it is the first function entry in the vtable \(\+16\)\. The typeinfo pointer and offset\-to\-top live at negative offsets from the address point\. You can imagine, conceptually, that the compiler is turning this: ``` struct Base { virtual void foo(); int x; }; ``` Into something like: ``` struct Base { void** __vptr; // Points to vtable int x; }; ``` ## Inheritance: Vtable Extension When a derived class overrides virtual functions, it gets its own vtable based on the base class layout: ``` struct Derived : Base { void foo() override; virtual void baz(); }; ``` ``` vtable for Derived: [offset-to-top] = 0 [typeinfo] = &typeinfo for Derived [foo()] = &Derived::foo (overridden) [bar()] = &Base::bar (inherited) [~Derived()] = &Derived::~Derived (complete object destructor) [~Derived()] = &Derived::~Derived (deleting destructor) [baz()] = &Derived::baz (new virtual function) ``` The inherited slots stay in the same positions\. The function at offset 0 is still`foo\(\)`, it just points to a different implementation\. So`foo\(\)`is replaced with`Derived::foo`,`bar\(\)`still points to the base implementation, and`baz\(\)`is appended at the end\. ## Virtual Function Calls: The Dispatch Mechanism Now let’s see what actually happens when you call a virtual function: ``` void call_foo(Base* b) { b->foo(); } ``` The compiler transforms this roughly into: ``` void call_foo(Base* b) { // Load the vptr from the object void** vtable = *(void***)b; // Load the function pointer from the vtable // foo() is at index 0 void (*func)(Base*) = (void(*)(Base*))vtable[0]; // Call the function func(b); } ``` Here’s what GCC generated for`call\_foo`with`\-Os`: ``` call_foo(Base*): movq (%rdi), %rax # load vptr jmp *(%rax) # call first function in vtable ``` If`foo`was the second function in the vtable, the call would instead look like: ``` jmp *8(%rax) # call second function in vtable ``` Because`call\_foo`doesn’t need to do anything after the virtual function returns, GCC chose to turn this into a tail call \(jmp instead of call\)\. Neat\. ## Multiple Inheritance: One Object, Multiple Vptrs So far, the model has been simple: one vptr at the start of the object, and one vtable corresponding to the object\. Multiple inheritance adds more complexity on top\. Consider: ``` struct Left { virtual void left_func() {} int left_data; }; struct Right { virtual void right_func() {} int right_data; }; struct MultiDerived : Left, Right { void left_func() override {} [[gnu::noinline]] void right_func() override { right_data = 42; } }; int main() { MultiDerived md; MultiDerived* md_ptr = &md; Right* r_ptr = md_ptr; // cast adds 16 bytes __builtin_printf("MultiDerived* : %p\n", (void*)md_ptr); __builtin_printf("Right* : %p\n", (void*)r_ptr); } ``` A`MultiDerived`object has to work as both a`Left\*`and a`Right\*`\. This means it contains a`Left`part and a`Right`part\. Those embedded base\-class parts are what the ABI calls**base subobjects**\. The actual layout is something like this: ``` struct MultiDerived { // Left base subobject void** __vptr_Left; int left_data; // Right base subobject void** __vptr_Right; int right_data; }; ``` If the compiler has a`MultiDerived\*`, it knows this layout at compile time, so accesses like`md\_ptr\-\>left\_data`and`md\_ptr\-\>right\_data`are just fixed offsets from the start of the complete object\. The tricky part is making the base pointers work too: - `Left\*`should point to the`Left`part of the object - `Right\*`should point to the`Right`part of the object In this example, the`Left`part is at offset 0, so`MultiDerived\*`and`Left\*`have the same address\. The`Right`part is 16 bytes later, so casting a`MultiDerived\*`to`Right\*`will add 16 bytes to the pointer\. That is what the printf of the pointer demonstrates\. Because both base subobjects are polymorphic, a`Left\*`must be able to find a`Left`\-compatible vtable from the start of the`Left`part, and a`Right\*`must be able to find a`Right`\-compatible vtable from the start of the`Right`part\. That is why the object has two vptrs\. Let’s inspect the layout using GCC’s`\-fdump\-lang\-class`to see how this is implemented: ``` Vtable for MultiDerived MultiDerived::_ZTV12MultiDerived: 7 entries 0 (int (*)(...))0 8 (int (*)(...))(& _ZTI12MultiDerived) 16 (int (*)(...))MultiDerived::left_func 24 (int (*)(...))MultiDerived::right_func 32 (int (*)(...))-16 40 (int (*)(...))(& _ZTI12MultiDerived) 48 (int (*)(...))MultiDerived::_ZThn16_N12MultiDerived10right_funcEv Class MultiDerived size=32 align=8 base size=28 base align=8 MultiDerived (0x0x23aa000) 0 vptr=((& MultiDerived::_ZTV12MultiDerived) + 16) Left (0x0x2311960) 0 primary-for MultiDerived (0x0x23aa000) Right (0x0x23119c0) 16 vptr=((& MultiDerived::_ZTV12MultiDerived) + 48) ``` The class dump shows that there are**two vptrs**, one at offset 0 for the`Left`part and another at offset 16 for the`Right`part\. Both vptrs point into the same`\_ZTV12MultiDerived`symbol at different offsets\. This symbol is a**virtual table group**: a primary virtual table followed by one or more**secondary virtual tables**, one per non\-primary base class: **Primary virtual table**\(for`Left`, entries at offsets 0\-24\): ``` Offset 0: offset-to-top = 0 Offset 8: typeinfo pointer Offset 16: left_func = &MultiDerived::left_func Offset 24: right_func = &MultiDerived::right_func ``` **Secondary virtual table**\(for`Right`, entries at offsets 32\-48\): ``` Offset 32: offset-to-top = -16 Offset 40: typeinfo pointer Offset 48: right_func = &MultiDerived::_ZThn16_N12MultiDerived10right_funcEv (thunk) ``` The`Right`subobject lives at offset 16 within`MultiDerived`\. When you cast`MultiDerived\*`to`Right\*`, the compiler adds 16 bytes to the pointer so that member accesses e\.g\.`r\-\>right\_data`\(which are fixed offsets from`this`\) land on the correct fields\. The pointer now points to the`Right`vptr, which points into the corresponding virtual table\. The secondary virtual table is the interesting one\. A call through`Right\*`starts from a pointer to the`Right`part of the object, so it has to use the`Right`vptr and the`Right`view of the virtual table\. The`right\_func`slot in the`Right`secondary virtual table doesn’t point directly at`MultiDerived::right\_func`\. Instead it points at a**non\-virtual thunk**: a tiny wrapper that adjusts`this`from the`Right`subobject back to the complete object, then calls the real implementation\. Because that adjustment is fixed for this class layout, it can be hardcoded into the thunk\. Hence it is “non\-virtual”\. Its mangled name breaks down as: - `\_Z`\- Itanium mangling prefix - `T`\- start of a special\-name \(same meaning as in`TV`for vtables\) - `h`\- non\-virtual call\-offset thunk \(virtual thunks are`v`instead\) - `n16`\- the adjustment:`n`prefix means negative, so \-16 - `\_`\- end of call\-offset - `N12MultiDerived10right\_funcEv`\- the target function \(`MultiDerived::right\_func\(\)`\) GCC emits this code: ``` .set .LTHUNK0,_ZN12MultiDerived10right_funcEv _ZThn16_N12MultiDerived10right_funcEv: subq $16, %rdi jmp .LTHUNK0 ``` In C\+\+, this is roughly: ``` void non_virtual_thunk_to_MultiDerived_right_func(Right* this) { MultiDerived* complete = (MultiDerived*)((char*)this - 16); MultiDerived::right_func(complete); } ``` The`\.set`creates a local alias for`MultiDerived::right\_func`\. The thunk then calls that alias rather than the external symbol directly, which avoids going through the PLT on Linux\. The**offset\-to\-top**field lets runtime code find the start of the complete object from a base subobject pointer\. The main user is`dynamic\_cast`:`dynamic\_cast<void\*\>\(r\)`reads`offset\-to\-top`from the secondary vtable and adds it to`this`to find the most\-derived object\. To summarize, a suitable multiple\-inheritance mental model is: - one object can contain multiple polymorphic base\-class parts - converting to a base pointer may change the address - each polymorphic base part gets its own vptr and vtable view - calls through a non\-primary base may need a thunk to repair`this`using a hardcoded adjustment - `offset\-to\-top`lets runtime code recover the start of the complete object from any base subobject pointer ## Virtual Inheritance Virtual inheritance is C\+\+’s solution to the[diamond problem](https://en.wikipedia.org/wiki/Multiple_inheritance#The_diamond_problem)\. Consider: ``` struct Base { virtual void f(); int x; }; struct Left : virtual Base { virtual void g(); }; struct Right : virtual Base { virtual void h(); }; struct Derived : Left, Right { void f() override; }; ``` The`virtual`keyword on`Left : virtual Base`and`Right : virtual Base`tells the compiler that`Base`is a*virtual base*: all paths through the inheritance graph share a single`Base`subobject\. Without it,`Derived`would contain two separate`Base`copies \(one through`Left`and one through`Right`\), so accessing`Base::x`through`Derived`would be ambiguous\. This is the diamond problem\. Sharing the base is what we want, but it breaks the assumption behind the previous section’s thunks\. With ordinary multiple inheritance, every base subobject sits at a fixed offset from the complete object, so a non\-virtual thunk can hardcode the adjustment\. With virtual inheritance, the offset to a virtual base depends on the final most\-derived type\.`Left`does not know where`Base`will end up when embedded inside`Derived`\. That is only known when laying out the complete object\. That has several consequences: - dispatch through a virtual base needs a runtime\-determined`this`adjustment - construction cannot assume each base’s standalone vtable layout - destruction has to avoid destroying the shared base more than once The next sections will cover the various strategies the ABI uses to deal with this\. ### Virtual Thunks In the previous section, a non\-virtual thunk hardcoded the adjustment \(`subq $16, %rdi`\)\. With virtual inheritance that is not possible because the offset to a virtual base depends on the complete type\. The fix is two new kinds of vtable slot, and a thunk that reads from them at runtime\. Let’s look at the GCC output for the`Derived`hierarchy first: ``` Class Derived size=32 align=8 base size=16 base align=8 Derived (0x0x77a8cf3dc000) 0 vptridx=0 vptr=((& Derived::_ZTV7Derived) + 24) Left (0x0x77a8cf20e3a8) 0 nearly-empty primary-for Derived (0x0x77a8cf3dc000) subvttidx=8 Base (0x0x77a8cf3d52a0) 16 virtual vptridx=40 vbaseoffset=-24 vptr=((& Derived::_ZTV7Derived) + 96) Right (0x0x77a8cf20e410) 8 nearly-empty subvttidx=24 vptridx=48 vptr=((& Derived::_ZTV7Derived) + 64) Base (0x0x77a8cf3d52a0) alternative-path ``` The layout is: - offset 0:`Left`subobject - offset 8:`Right`subobject - offset 16: shared`Base`subobject \(vptr at 16,`int x`at 24\) The vtable group for`Derived`has three sections: ``` Vtable for Derived Derived::_ZTV7Derived: 13 entries 0 16 <- vptr[-3]: vbase offset (Base is +16 from Left's vptr) 8 (int (*)(...))0 <- vptr[-2]: offset-to-top = 0 16 (int (*)(...))(& _ZTI7Derived) <- vptr[-1]: typeinfo 24 (int (*)(...))Left::g <- vptr[0]: address point 32 (int (*)(...))Derived::f <- vptr[1] 40 8 <- vptr[-3]: vbase offset (Base is +8 from Right's vptr) 48 (int (*)(...))-8 <- vptr[-2]: offset-to-top = -8 56 (int (*)(...))(& _ZTI7Derived) <- vptr[-1]: typeinfo 64 (int (*)(...))Right::h <- vptr[0]: address point 72 -16 <- vptr[-3]: vcall offset for Base's f() thunk 80 (int (*)(...))-16 <- vptr[-2]: offset-to-top = -16 88 (int (*)(...))(& _ZTI7Derived) <- vptr[-1]: typeinfo 96 (int (*)(...))Derived::_ZTv0_n24_N7Derived1fEv <- vptr[0]: virtual thunk for f() ``` The`Left`and`Right`sections each start with a**vbase offset**at`vptr\[\-3\]`\. A vbase offset is used to navigate*from a subobject to its virtual base*— it answers “where is`Base`relative to this vptr?” For`Left`that is`\+16`; for`Right`,`\+8`\. Any code that needs to reach`Base`from a`Left\*`or`Right\*`reads this slot at runtime\. The`Base`section has a**vcall offset**at`vptr\[\-3\]`instead:`\-16`\. A vcall offset navigates in the opposite direction —*from the virtual base back to the complete object*\. It is read exclusively by virtual thunks to adjust a`Base\*`back up to`Derived`before jumping to the actual implementation: Now, lets have a look at generated code for a virtual thunk: ``` virtual thunk to Derived::f(): # _ZTv0_n24_N7Derived1fEv movq (%rdi), %r10 # load vptr addq -24(%r10), %rdi # read vcall offset from vptr[-3], adjust this jmp .LTHUNK0 # jump to Derived::f() ``` In C\+\+, this is roughly: ``` void virtual_thunk_to_Derived_f(Base* this) { void** vptr = *(void***)this; this = (Base*)((char*)this + ((ptrdiff_t*)vptr)[-3]); // read vcall offset Derived::f(this); } ``` The virtual thunk’s mangled name \(\_ZTv0\_n24\_N7Derived1fEv\) breaks down like this: - `Tv`— virtual thunk - `0`— no non\-virtual`this`adjustment before reading the vcall offset - `n24`— vcall offset is 24 bytes before the address point \(`vptr\[\-3\]`\) - `N7Derived1fEv`— target is`Derived::f\(\)` Each vtable section has one negative slot per virtual base: a vbase offset in the non\-virtual\-base sections, and a vcall offset in the virtual base’s own section\. A class can be both a virtual base and have virtual bases of its own, in which case its section carries vbase offsets first \(one per virtual base it inherits\), then vcall offsets \(one per virtual function that needs a thunk\)\. ### Construction: Yet another table C\+\+ guarantees virtual dispatch works inside constructor bodies, so the vptr must be valid before the constructor body runs\. But, we established prior that`Left`doesn’t know where`Base`will land until the final complete type is laid out\. `Left`’s vtable records what it knew at compile time: in a standalone`Left`,`Base`is 8 bytes away\. When embedded in`Derived`,`Base`ends up 16 bytes away\.`Left`’s constructor can only install its own vtable, which still says`8`, so a virtual thunk firing during`Left`’s construction inside`Derived`will compute the wrong address for`Base`\. The fix is a**construction vtable**: a context\-specific copy of`Left`’s vtable with offsets corrected for the actual complete object being built\. It is installed into the vptr before`Left`’s constructor body runs, and overwritten with the real vtable once`Left`’s constructor returns\. GCC emits one for every base class that has virtual bases — for this hierarchy,`\_ZTC7Derived0\_4Left`and`\_ZTC7Derived8\_5Right`\. The mangled name`\_ZTC7Derived0\_4Left`breaks down as: - `TC`— construction vtable - `7Derived`— complete class - `0`— byte offset of`Left`within`Derived` - `4Left`— base class `\_ZTC7Derived8\_5Right`is the same thing:`Right`sits at offset`8`in`Derived`\. Now there needs to be a way to deliver the right construction vtable to each base constructor\. That’s the purpose of the**VTT**\(Virtual Table Table\): an array of vtable pointers for a specific construction context, one entry per subobject vptr that needs to be set\. Constructors for classes with virtual bases come in two variants, similar to destructors: - **C1**\(complete\-object constructor\): what you call to create a standalone object\. Constructs virtual bases first, then calls each non\-virtual base’s constructor as**C2**, and finally installs the final vtable pointers for all subobjects\. - **C2**\(base\-object constructor\): what C1 calls for each base subobject\. Does not construct virtual bases — the outermost C1 owns them\. If the class has virtual bases, receives a**sub\-VTT**\(a contiguous slice of the VTT with one entry per vptr in that subobject\) and installs the construction vtable pointers from it for the duration of construction\. `Left`and`Right`both have`Base`as a virtual base, so their C2s receive a sub\-VTT\.`Base`has no virtual bases, therefore it has no C2/C1 split and no sub\-VTT\. ``` VTT for Derived Derived::_ZTT7Derived: 7 entries 0 ((& Derived::_ZTV7Derived) + 24) <- [0] Derived's own vtable (used if Derived is itself a subobject) 8 ((& Derived::_ZTC7Derived0_4Left) + 24) <- [1] sub-VTT for Left::C2: Left's vptr 16 ((& Derived::_ZTC7Derived0_4Left) + 56) <- [2] Base's vptr 24 ((& Derived::_ZTC7Derived8_5Right) + 24) <- [3] sub-VTT for Right::C2: Right's vptr 32 ((& Derived::_ZTC7Derived8_5Right) + 56) <- [4] Base's vptr 40 ((& Derived::_ZTV7Derived) + 96) <- [5] permanent vptr for Base 48 ((& Derived::_ZTV7Derived) + 64) <- [6] permanent vptr for Right ``` The VTT has three logical regions: - **`\[0\]`**—`Derived`’s own primary vtable pointer\. Only used if`Derived`is itself a base of another class; unused here\. - **`\[1\]`\-`\[4\]`**— sub\-VTT slices, one per base class that needs one\. Each is a contiguous block of vtable pointers passed to that base’s C2\.`Left`’s slice is`\[1\]`\-`\[2\]`\(its own vptr \+`Base`’s vptr\);`Right`’s is`\[3\]`\-`\[4\]`\. - **`\[5\]`\-`\[6\]`**— the permanent vptrs into`Derived`’s own vtable, written once all base constructors finish\.`Base`and`Right`each get a slot\.`Left`doesn’t — it is`Derived`’s primary base, so their vptrs share the same memory location at offset 0, and installing`Derived`’s vtable there covers both at once\. In pseudocode, the flow is: ``` // Left's C2 — installs whichever construction vtables the caller supplies void Left_C2(Left* this, void** vtt) { this->vptr = vtt[0]; // Left's construction vtable ptrdiff_t off = ((ptrdiff_t*)this->vptr)[-3]; // vbaseoffset ((Base*)((char*)this + off))->vptr = vtt[1]; // Base's construction vtable // ... constructor body ... } // Derived's C1 — drives the whole sequence void Derived_C1(Derived* this) { base->vptr = &Base::vtable; // seed Base with something valid Left_C2 ((Left*)this, &VTT[1]); // Left uses VTT[1..2] Right_C2((Right*)(this + 8), &VTT[3]); // Right uses VTT[3..4] // Install the final vtable pointers once all bases are constructed left->vptr = &Derived::vtable[Left_section]; right->vptr = &Derived::vtable[Right_section]; base->vptr = &Derived::vtable[Base_section]; // ... constructor body ... } ``` Now, lets look at the actual assembly for`Left::Left`C2: ``` Left::Left() [base object constructor]: # _ZN4LeftC2Ev movq (%rsi), %rax # read Left's construction vtable ptr from VTT movq %rax, (%rdi) # install it into Left's vptr movq -24(%rax), %rax # read vbaseoffset: byte offset to Base's vptr within this object movq 8(%rsi), %rdx # read Base's construction vtable ptr from VTT movq %rdx, (%rdi,%rax) # install it into Base's vptr # ... constructor body ... ret ``` And`Derived::Derived`C1, which drives the whole sequence: ``` Derived::Derived() [complete object constructor]: # _ZN7DerivedC1Ev movq %rdi, %rbx leaq 16+_ZTV4Base(%rip), %rax movq %rax, 16(%rdi) # construct Base: install its vtable at offset 16 leaq 8+_ZTT7Derived(%rip), %rbp # %rbp = &VTT[1] movq %rbp, %rsi call _ZN4LeftC2Ev # Left::C2(this, &VTT[1]) leaq 16(%rbp), %rsi # %rsi = &VTT[3] leaq 8(%rbx), %rdi call _ZN5RightC2Ev # Right::C2(this+8, &VTT[3]) leaq 24+_ZTV7Derived(%rip), %rax movq %rax, (%rbx) # install final vtable into Left/Derived's vptr (offset 0) leaq 72(%rax), %rax movq %rax, 16(%rbx) # install into Base's vptr (offset 16) leaq -32(%rax), %rax movq %rax, 8(%rbx) # install into Right's vptr (offset 8) # ... constructor body ... ret ``` Virtual bases are constructed before non\-virtual bases, so`Base`goes first\.`Base`has no virtual bases, so GCC inlines its vtable install directly rather than emitting a constructor call\. Then`Left\_C2`and`Right\_C2`each run with their corresponding sub\-VTT\. Both install a construction vtable pointer into`Base`’s vptr, using the entry from their own sub\-VTT\. Both sub\-VTTs point into construction vtables that have the vcall offsets corrected for`Base`’s actual position inside`Derived`\. Once both C2s finish, C1 installs`Derived`’s final vtable into all three vptrs\. ### Destruction and`D2` Destruction has a similar problem: the shared`Base`subobject must only be destroyed once\. When you destroy a standalone`Left`,`Left::~Left`destroys`Base`too, because there is nothing else to do it\. But when you destroy a`Derived`,`Derived::~Derived`takes responsibility for`Base`, and the`Left`and`Right`destructors must skip it\. That is the role of the aforementioned`D2`\(the base\-object destructor\): destroy this subobject only, and leave virtual bases alone\. The call chain for`delete derived\_ptr`: - `D0`\(deleting destructor\): calls`D1`, then`operator delete` - `D1`\(complete\-object destructor\): destroys`Derived`’s own members, calls`Left::~Left`and`Right::~Right`as`D2`, then destroys`Base` - `D2`\(base\-object destructor\): destroys the subobject’s own members and stops — virtual bases are left alone The most\-derived destructor owns virtual bases\. Everything else uses`D2`\. ## Conclusion I spent a long digging through compiler output and the ABI spec to understand this properly, and this blog post is the result of that research\. Hopefully writing it all down makes it easier for other people to understand this topic, too\. If this post helped you understand something that was previously unclear, I’d love to hear about it in the comments\. Equally, if I got something wrong or oversimplified something in a misleading way, please leave a correction\. If my explanations didn’t quite click, Nimrod has written two posts covering part of this topic that might work better for you: - [What does C\+\+ Object Layout Look Like?](https://nimrod.blog/posts/what-does-cpp-object-layout-look-like/) - [C\+\+ Virtual Table Tables \(VTT\)](https://nimrod.blog/posts/cpp-virtual-table-tables/) For the low\-level details in standardese, the official Itanium C\+\+ ABI specification is available at[https://itanium\-cxx\-abi\.github\.io/cxx\-abi/abi\.html](https://itanium-cxx-abi.github.io/cxx-abi/abi.html)\.

Similar Articles

Thoroughly Understanding C++ ABI

Hacker News Top

A deep dive into C++ ABI, explaining binary interface concepts, CPU/OS dependencies, object file formats like ELF and PE32+, and calling conventions.

When can the C++ compiler devirtualize a call?

Hacker News Top

Explores when C++ compilers can devirtualize virtual function calls, covering cases like known dynamic types and final keyword, with comparisons across GCC, Clang, MSVC, and ICC.

Bytecode VMs in surprising places (2024)

Hacker News Top

This article explores surprising uses of bytecode virtual machines, specifically eBPF in the Linux kernel and DWARF expressions for debug information in compiled binaries.

Pushing the limits of RISC-V emulation

Hacker News Top

This blog post explores how to make RISC-V execution faster on non-RISC-V machines using an ahead-of-time recompiler that connects basic blocks with tail calls and leverages Clang's preserve_none calling convention, as part of the OpenVM project at Axiom.