Understanding C++ Multiple Inheritance: What Goes Into the A and B Parts of a vtable
The article explains how clang can dump the exact vtable layout for a class with multiple inheritance, revealing that a single vtable is actually a virtual table group composed of a primary and secondary table, each containing its own offset_to_top, RTTI entries, function slots and thunks, and shows why new virtual functions appear only in the primary part while the secondary part holds adjusted thunks for overridden base functions.
Most articles draw a simplistic picture of a multiple‑inheritance vtable as two side‑by‑side boxes, but that hides crucial details: the field order in the A and B sections differs, the B section contains a slot that is not a function body, and C’s newly added virtual functions only appear in one side.
Clang can print the exact layout
Running clang with the internal debugging switch
clang++ -std=c++17 -Xclang -fdump-vtable-layouts -emit-llvm -c vt.cpp -o /dev/nullprints the vtable for class C instead of a diagram. The dump looks like this:
Vtable for 'C' (10 entries)
0 | offset_to_top (0)
1 | C RTTI
-- (A, 0) vtable address --
-- (C, 0) vtable address --
2 | void C::a1()
3 | void A::a2()
4 | void C::b1()
5 | void C::c1()
6 | offset_to_top (-16)
7 | C RTTI
-- (B, 16) vtable address --
8 | void C::b1()
[this adjustment: -16 non-virtual] method: void B::b1()
9 | void B::b2()The first thing to notice is that there are two offset_to_top entries (indices 0 and 6) and two C RTTI entries (indices 1 and 7). A single table should not contain two copies of these fields, which means the dump actually represents two tables concatenated together.
Virtual table group
In the Itanium C++ ABI the proper term is virtual table group : a primary virtual table followed by one or more secondary virtual tables placed contiguously in memory. The dump above is the group for C, consisting of:
Indices 0‑5 : the primary virtual table, corresponding to C 's primary base A. This is the “A part”.
Indices 6‑9 : the secondary virtual table for base B. This is the “B part”.
The ABI defines the primary base as “the unique base class (if any) that shares the virtual‑table pointer at offset 0”. Because A is the first polymorphic base, its sub‑object sits at offset 0 in C, so its vptr is the object’s vptr.
A part – primary vtable
Extracting indices 0‑5 gives:
0 | offset_to_top (0)
1 | C RTTI
-- (A, 0) vtable address --
-- (C, 0) vtable address --
2 | void C::a1() // overrides A::a1
3 | void A::a2() // not overridden
4 | void C::b1() // C’s override of B::b1 appears here
5 | void C::c1() // brand‑new virtual function offset_to_topis 0 because the A sub‑object starts at the object’s top; it is used by dynamic_cast<void*> and RTTI to compute the address of the most‑derived object.
The RTTI entry points to C, not A. All sub‑tables in a group store the RTTI of the most‑derived type so that typeid(*p) yields the real dynamic type regardless of whether the pointer is an A* or B*.
Function slots follow the classic single‑inheritance rule: each virtual function gets a fixed slot number. Overridden functions replace the pointer in that slot, while untouched functions keep the base implementation. This guarantees that code compiled against A (e.g., A* p; p->a1();) always calls the same slot.
Two slots are special to multiple inheritance:
Slot 5 holds C::c1, a new virtual function introduced by C. Because the primary vptr is shared by A and C, the new function is appended after the existing A slots.
Slot 4 contains C::b1, an override of B 's virtual function. Even though b1 originates from B, the call through an A* (which uses the primary vptr) lands in the primary table, so the overridden implementation is placed here.
B part – secondary vtable
Extracting indices 6‑9 gives:
6 | offset_to_top (-16)
7 | C RTTI
-- (B, 16) vtable address --
8 | void C::b1()
[this adjustment: -16 non-virtual] method: void B::b1()
9 | void B::b2()Here offset_to_top is –16 because the B sub‑object starts 16 bytes after the object’s top (the layout is [vptr_A][ax][pad][vptr_B][bx][cx]). The ABI explicitly states that secondary tables have a negative offset_to_top.
The RTTI entry again points to C, ensuring that a B* also reports the most‑derived type.
Slot 9 holds B::b2, unchanged from the standalone B layout. Slot 8, however, does not contain a direct pointer to C::b1. Instead it contains a **non‑virtual thunk** whose job is to adjust the this pointer by –16 before jumping to C::b1. The ABI defines a thunk as “a segment of code that modifies parameters (e.g., this) before transferring control to the target function”. The generated thunk looks like:
; c++filt __ZThn16_N1C2b1Ev => non-virtual thunk to C::b1()
__ZThn16_N1C2b1Ev:
...
mov rdi, qword ptr [rbp-8]
add rdi, -16 ; this -= 16, turning B* into C*
...
jmp __ZN1C2b1Ev ; tail‑call to C::b1The thunk is necessary because code compiled against B expects the this pointer to refer to the B sub‑object. The overridden implementation lives in C, which expects a C*. The thunk bridges the gap by correcting the pointer.
Notice that the primary part’s slot 4 already contains C::b1 directly; the secondary part still needs the thunk because callers using the B vptr will land in the secondary table first.
Why new virtual functions cannot appear in the B part
When a caller has a B*, it knows only the slot numbers defined by B. Adding a new slot for C::c1 would either shift existing B slots (breaking existing code) or place the function after the last known slot (making it unreachable to B* callers). Therefore new virtual functions are always placed in the primary part, where the C vptr is also used for A* calls.
Choosing the primary base
The primary base is not simply the first base in the declaration list; it is the first *polymorphic* base that can share the vptr at offset 0. If A were non‑polymorphic, B would become the primary base, its vptr would be at offset 0, and the “A part” would disappear.
Virtual inheritance
With virtual inheritance (e.g., struct C : virtual A, virtual B) the dump gains two extra columns: vcall offsets and vbase offsets. Those columns are used at runtime to locate virtual bases and to adjust this for virtual base overrides, making the vtable layout considerably more complex. The article’s analysis covers only the simple non‑virtual case.
How to explore yourself
Use the following commands to inspect any class: clang++ -Xclang -fdump-vtable-layouts – prints the virtual table group. -Xclang -fdump-record-layouts – shows actual field offsets inside the object. nm combined with c++filt to demangle symbols like _ZTV (vtable), _ZThn (non‑virtual thunk), _ZTv (virtual thunk), and _ZTI (typeinfo).
Comparing the dumps for a non‑virtual and a virtual inheritance version reveals how the extra columns become populated, extending the simple “A part / B part” model into a third dimension.
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.
