[!IMPORTANT]
TL;DR — what you must remember: RTTI is the runtime type machinery: each polymorphic class has a vtable whose slot −1 points to a type_info. dynamic_cast walks that to do checked down/cross-casts, and typeid reads it. It only works on polymorphic types (≥1 virtual function), costs a runtime lookup, and is exactly what -fno-rtti strips. Contrast: static_cast is unchecked and compile-time; dynamic_cast is checked and runtime.
class Base { int base_data; public: virtual void f() {} }; class Derived : public Base { int derived_data; public: void f() override {} };
Base object in memory:
Address Content Size
------- ------- ----
0x1000 vptr -> VTable_Base 8 bytes
0x1008 base_data 4 bytes
0x100C padding 4 bytes
Total: 16 bytes
VTable_Base structure:
Address Content Offset from vtable start
------- ------- -------------------------
0x2000 offset-to-top (0) -16 bytes
0x2008 type_info ptr -> RTTI_Base -8 bytes
0x2010 &Base::f() 0 bytes (vtable[0])
RTTI_Base structure (_ZTI4Base):
struct type_info_base { void* vtable_ptr; // Points to type_info's own vtable const char* name; // "4Base" (mangled name) };
Derived object in memory:
Address Content Size
------- ------- ----
0x3000 vptr -> VTable_Derived 8 bytes
0x3008 base_data 4 bytes
0x300C padding 4 bytes
0x3010 derived_data 4 bytes
0x3014 padding 4 bytes
Total: 24 bytes
VTable_Derived:
0x4000 offset-to-top (0) -16 bytes
0x4008 type_info ptr -> RTTI_Derived -8 bytes
0x4010 &Derived::f() 0 bytes (vtable[0])
RTTI_Derived structure:
struct si_class_type_info { // si = single inheritance void* vtable_ptr; const char* name; // "7Derived" const type_info* base; // Pointer to RTTI_Base };
class A { int a_data; public: virtual void fa() {} }; class B { int b_data; public: virtual void fb() {} }; class C : public A, public B { int c_data; public: void fa() override {} void fb() override {} virtual void fc() {} };
C object in memory:
Address Content Size Notes
------- ------- ---- -----
0x5000 vptr_A -> VTable_C_A 8 Primary base (A subobject)
0x5008 a_data 4
0x500C padding 4
0x5010 vptr_B -> VTable_C_B 8 Secondary base (B subobject)
0x5018 b_data 4
0x501C padding 4
0x5020 c_data 4
0x5024 padding 4
Total: 40 bytes
Key insight: TWO vptrs! One for each base class with virtual functions.
VTable_C_A (primary):
Address Content Offset
------- ------- ------
0x6000 offset-to-top (0) -16
0x6008 RTTI_C -8
0x6010 &C::fa() 0 (vtable[0])
0x6018 &C::fc() 8 (vtable[1])
VTable_C_B (secondary):
Address Content Offset
------- ------- ------
0x6100 offset-to-top (-16) -16 ← NOTE: negative!
0x6108 RTTI_C -8
0x6110 &thunk_to_C::fb() 0 (vtable[0])
Why thunk? Because this pointer needs adjustment.
Thunk code:
thunk_to_C::fb(): sub rdi, 16 ; Adjust 'this' from B* to C* jmp C::fb() ; Jump to actual implementation
RTTI_C structure:
struct vmi_class_type_info { // vmi = virtual + multiple inheritance void* vtable_ptr; const char* name; // "1C" unsigned int flags; // Inheritance flags unsigned int base_count; // 2 struct base_info { const type_info* base_type; // RTTI_A or RTTI_B long offset_flags; // Offset + public/virtual flags } base_info[2]; }; // Actual values: { vtable: &type_info_vtable, name: "1C", flags: 0, base_count: 2, base_info: [ { base_type: &RTTI_A, offset_flags: 0x2 }, // offset=0, public { base_type: &RTTI_B, offset_flags: 0x4002 } // offset=16, public ] }
offset_flags encoding:
Bits 0-1: Inheritance flags (0x2 = public)
Bit 2: Virtual base flag (0 = non-virtual)
Bits 8+: Offset in bytes (shifted left by 8)
So 0x4002 = (16 << 8) | 0x2 = offset 16, public
class V { int v_data; public: virtual void fv() {} }; class A : public virtual V { int a_data; public: void fv() override {} }; class B : public virtual V { int b_data; public: void fv() override {} }; class D : public A, public B { int d_data; public: void fv() override {} };
This is complex. D has:
D object in memory:
Address Content Size Notes
------- ------- ---- -----
0x7000 vptr_A -> VTable_D_A 8 A subobject
0x7008 vbase_offset -> 32 8 Offset to V subobject
0x7010 a_data 4
0x7014 padding 4
0x7018 vptr_B -> VTable_D_B 8 B subobject
0x7020 vbase_offset -> 16 8 Offset to V from B
0x7028 b_data 4
0x702C padding 4
0x7030 d_data 4 D's data
0x7034 padding 4
0x7038 vptr_V -> VTable_D_V 8 V subobject (SHARED)
0x7040 v_data 4
0x7044 padding 4
Total: 72 bytes
VTable_D_A:
0x8000 vbase_offset (32) -24 ← offset to V from A
0x8008 offset-to-top (0) -16
0x8010 RTTI_D -8
0x8018 &D::fv() [A-in-D thunk] 0
VTable_D_B:
0x8100 vbase_offset (16) -24 ← offset to V from B
0x8108 offset-to-top (-24) -16
0x8110 RTTI_D -8
0x8118 &D::fv() [B-in-D thunk] 0
VTable_D_V:
0x8200 vbase_offset (0) -24
0x8208 offset-to-top (-56) -16 ← offset from V to D start
0x8210 RTTI_D -8
0x8218 &D::fv() 0 ← actual implementation
RTTI_D:
{ vtable: &type_info_vtable, name: "1D", flags: 1, // Has virtual bases base_count: 2, base_info: [ { base_type: &RTTI_A, offset_flags: 0x2 }, // offset=0, public, non-virtual { base_type: &RTTI_B, offset_flags: 0x1802 } // offset=24, public, non-virtual ] }
RTTI_A:
{ vtable: &type_info_vtable, name: "1A", flags: 1, // Has virtual bases base_count: 1, base_info: [ { base_type: &RTTI_V, offset_flags: 0x1006 } // Bit 2 set = VIRTUAL base ] }
Key: Virtual base offset stored in vtable AND base_info has virtual flag.
D* d = new D(); A* a = dynamic_cast<A*>(d);
This is trivial - no RTTI needed!
Compiler knows offset at compile time:
; Offset from D to A is 0 (A is first subobject) %a = bitcast %class.D* %d to %class.A*
No runtime call to __dynamic_cast.
A* a = get_some_object(); // Might be A or D D* d = dynamic_cast<D*>(a);
Now we need RTTI.
Generated code:
%d_raw = call i8* @__dynamic_cast( i8* %a, ; Source pointer i8* @_ZTI1A, ; Source type_info (RTTI_A) i8* @_ZTI1D, ; Destination type_info (RTTI_D) i64 0 ; Offset hint ) %d = bitcast i8* %d_raw to %class.D*
__dynamic_cast implementation (pseudocode):
void* __dynamic_cast( void* src_ptr, const type_info* src_type, const type_info* dst_type, ptrdiff_t src_to_dst_hint ) { // Step 1: Get actual object type from vptr void** vptr = *(void***)src_ptr; const type_info* actual_type = (type_info*)vptr[-1]; // Step 2: Check if actual type matches destination if (actual_type == dst_type) { // Object IS the destination type // Apply offset from src to dst within the object return (char*)src_ptr + src_to_dst_hint; } // Step 3: Walk inheritance tree // Is dst_type a base of actual_type? ptrdiff_t offset; if (is_base_of(dst_type, actual_type, &offset)) { return (char*)src_ptr + offset; } // Step 4: Cross-cast check // Maybe src and dst are siblings? if (is_public_base(src_type, actual_type) && is_public_base(dst_type, actual_type)) { // Calculate offset from src to dst through actual offset = offset_to_actual - offset_from_dst_to_actual; return (char*)src_ptr + offset; } return nullptr; // Cast failed }
Concrete example with D → A → V:
V* v = get_object(); // Actually points to D D* d = dynamic_cast<D*>(v);
Call:
__dynamic_cast(v, RTTI_V, RTTI_D, 0)
Steps:
v's vptr → points to VTable_D_Vvptr[-1] → get RTTI_Dvptr[-2] → get offset-to-top = -56v + (-56) → pointer to start of D objectis_base_of pseudocode:
bool is_base_of(const type_info* base, const type_info* derived, ptrdiff_t* offset) { if (derived == base) { *offset = 0; return true; } // Get base class info from derived vmi_class_type_info* vmi = (vmi_class_type_info*)derived; for (int i = 0; i < vmi->base_count; i++) { ptrdiff_t base_offset = vmi->base_info[i].offset_flags >> 8; bool is_virtual = vmi->base_info[i].offset_flags & 0x4; if (is_virtual) { // Read vbase offset from vtable void** vptr = /* ... read from object ... */; base_offset = *(ptrdiff_t*)(vptr - 3); // vbase_offset location } if (vmi->base_info[i].base_type == base) { *offset = base_offset; return true; } // Recursive check ptrdiff_t nested_offset; if (is_base_of(base, vmi->base_info[i].base_type, &nested_offset)) { *offset = base_offset + nested_offset; return true; } } return false; }
class A { virtual ~A() {} }; class B { virtual ~B() {} }; class C : public A, public B {}; A* a = new C(); B* b = dynamic_cast<B*>(a); // Cast from A to B (siblings in C)
Memory layout of C:
0x9000 vptr_A -> VTable_C_A 8
0x9008 vptr_B -> VTable_C_B 8
Dynamic cast call:
__dynamic_cast(a, RTTI_A, RTTI_B, -1) // -1 = no hint
Algorithm:
a → VTable_C_Aa + 8Result: b points 8 bytes after a.
V* v = new D(); // V is virtual base of D A* a = dynamic_cast<A*>(v); // Cast from V to A
Call:
__dynamic_cast(v, RTTI_V, RTTI_A, -1)
Algorithm:
v → VTable_D_V (at offset 56 in D)v + (-56) = 0x7000v + (-56) + 0 = 0x7000 (start of D, which is also start of A)Key: Had to use offset-to-top to find most-derived object, then search from there.
class X { virtual ~X() {} }; class Y { virtual ~Y() {} }; X* x = new X(); Y* y = dynamic_cast<Y*>(x); // Unrelated types
Call:
__dynamic_cast(x, RTTI_X, RTTI_Y, -1)
Algorithm:
class Base { int base_val; public: virtual ~Base() {} virtual void identify() { printf("Base\n"); } }; class Interface { public: virtual ~Interface() {} virtual void interface_method() = 0; }; class Middle : public Base, public Interface { int middle_val; public: void identify() override { printf("Middle\n"); } void interface_method() override { printf("Middle impl\n"); } }; class Derived : public Middle { int derived_val; public: void identify() override { printf("Derived\n"); } }; // Test dynamic_cast void test() { Derived* d = new Derived(); // Cast 1: Upcast to Base Base* b = dynamic_cast<Base*>(d); // Cast 2: Upcast to Interface Interface* i = dynamic_cast<Interface*>(d); // Cast 3: Cross-cast from Base to Interface Interface* i2 = dynamic_cast<Interface*>(b); // Cast 4: Downcast from Interface to Derived Derived* d2 = dynamic_cast<Derived*>(i); }
Derived object:
Address Content Size Cumulative
------- ------- ---- ----------
0xA000 vptr_Base -> VT_D_Base 8 0
0xA008 base_val 4 8
0xA00C padding 4 12
0xA010 vptr_Iface -> VT_D_Iface 8 16
0xA018 middle_val 4 24
0xA01C padding 4 28
0xA020 derived_val 4 32
0xA024 padding 4 36
Total: 40 bytes
VT_D_Base (primary):
0xB000 offset-to-top (0) -16
0xB008 &RTTI_Derived -8
0xB010 &Derived::~Derived() 0 (vtable[0])
0xB018 &Derived::identify() 8 (vtable[1])
VT_D_Iface (secondary):
0xB100 offset-to-top (-16) -16 ← Key value!
0xB108 &RTTI_Derived -8
0xB110 &thunk_Derived::~Derived() 0
0xB118 &thunk_Derived::interface_method() 8
Thunk example:
thunk_Derived::interface_method: sub rdi, 16 ; this -= 16 (Interface* → Derived*) jmp Derived::interface_method
RTTI_Base:
{ vtable: &std::type_info::vtable, name: "4Base" }
RTTI_Interface:
{ vtable: &std::type_info::vtable, name: "9Interface" }
RTTI_Middle:
{ vtable: &std::type_info::vtable, name: "6Middle", flags: 0, base_count: 2, base_info: [ { base_type: &RTTI_Base, offset_flags: 0x0002 }, // offset 0 { base_type: &RTTI_Interface, offset_flags: 0x1002 } // offset 16 ] }
RTTI_Derived:
{ vtable: &std::type_info::vtable, name: "7Derived", flags: 0, base_count: 1, base_info: [ { base_type: &RTTI_Middle, offset_flags: 0x0002 } // offset 0 ] }
Base* b = dynamic_cast<Base*>(d);
Compile-time resolution:
%b = bitcast %Derived* %d to %Base*Result: b = 0xA000 (same address as d)
Interface* i = dynamic_cast<Interface*>(d);
Compile-time resolution:
%i = getelementptr %Derived* %d, i32 0, i32 1 (simplified)Result: i = 0xA010 (d + 16 bytes)
Verify vptr:
*(void**)0xA010 → 0xB110 (VT_D_Iface + 16)0xB110[-1] → points to RTTI_Derived ✓Interface* i2 = dynamic_cast<Interface*>(b);
This needs RTTI!
Generated IR:
%i2_raw = call i8* @__dynamic_cast( i8* %b, ; 0xA000 i8* @RTTI_Base, ; Source type i8* @RTTI_Interface, ; Target type i64 -1 ; No hint )
__dynamic_cast execution:
Step 1: Get actual type
void** vptr = *(void***)0xA000; // Read vptr from b // vptr = 0xB010 (VT_D_Base + 16) type_info* actual = (type_info*)vptr[-1]; // actual = &RTTI_Derived
Step 2: Is target == actual?
if (&RTTI_Interface == &RTTI_Derived) → false
Step 3: Is target a base of actual?
is_base_of(&RTTI_Interface, &RTTI_Derived, &offset) // Search RTTI_Derived bases: // base_info[0] = RTTI_Middle at offset 0 // Recursively search RTTI_Middle: is_base_of(&RTTI_Interface, &RTTI_Middle, &offset) // base_info[1] = RTTI_Interface at offset 16 ← FOUND! // Total offset: 0 (Derived→Middle) + 16 (Middle→Interface) = 16 offset = 16 return true
Step 4: Apply offset
return (char*)src_ptr + offset; // return 0xA000 + 16 = 0xA010
Result: i2 = 0xA010
Verification:
i2 points to Interface subobject*(void**)i2 → 0xB110 (VT_D_Iface)i from Cast 2 ✓Derived* d2 = dynamic_cast<Derived*>(i);
i = 0xA010 (Interface subobject)
Generated IR:
%d2_raw = call i8* @__dynamic_cast( i8* %i, ; 0xA010 i8* @RTTI_Interface, ; Source i8* @RTTI_Derived, ; Target i64 -1 )
__dynamic_cast execution:
Step 1: Get actual type
void** vptr = *(void***)0xA010; // vptr = 0xB110 (VT_D_Iface + 16) type_info* actual = (type_info*)vptr[-1]; // actual = &RTTI_Derived
Step 2: Is target == actual?
if (&RTTI_Derived == &RTTI_Derived) → TRUE!
Step 3: Apply offset-to-top
ptrdiff_t offset_to_top = *(ptrdiff_t*)(vptr - 2); // Read from 0xB100 (vptr - 16) // offset_to_top = -16 return (char*)src_ptr + offset_to_top; // return 0xA010 + (-16) = 0xA000
Result: d2 = 0xA000 (points to start of Derived object)
Verification:
d2 == d ✓*(void**)d2 → 0xB010 (VT_D_Base) ✓class Base { virtual ~Base() {} }; class Derived : private Base {}; // private! Derived* d = new Derived(); Base* b = dynamic_cast<Base*>(d); // Compile error!
Why fails: Compile-time check. Private bases not accessible.
In RTTI: offset_flags encodes access level:
0x0 = private
0x1 = protected
0x2 = public
__dynamic_cast checks this bit. If not public, returns nullptr.
class A { virtual ~A() {} }; class B : public A {}; class C : public A {}; class D : public B, public C {}; // Diamond, non-virtual! D* d = new D(); A* a = dynamic_cast<A*>(d); // Compile error: ambiguous
Why: D has TWO A subobjects (via B and via C).
Memory:
D object:
B subobject:
A subobject ← A #1
C subobject:
A subobject ← A #2
Solution: Use virtual inheritance or cast to B first:
B* b = d; A* a = dynamic_cast<A*>(b); // OK, unambiguous
Derived* d = new Derived(); void* v = dynamic_cast<void*>(d);
Special case: Returns pointer to most-derived object.
Implementation:
// Just apply offset-to-top void** vptr = *(void***)src_ptr; ptrdiff_t offset = *(ptrdiff_t*)(vptr - 2); return (char*)src_ptr + offset;
Use case: Get start address regardless of subobject pointer.
Upcast: Free (compile-time offset) Downcast/Cross-cast:
Typical cost: 50-200 CPU cycles
Optimization 1: Cache results
// Bad: repeated dynamic_cast for (...) { if (Derived* d = dynamic_cast<Derived*>(base)) { d->method(); } } // Good: cast once Derived* d = dynamic_cast<Derived*>(base); if (d) { for (...) { d->method(); } }
Optimization 2: Avoid dynamic_cast in hot paths
// Use visitor pattern or type enum instead class Base { enum Type { TYPE_DERIVED1, TYPE_DERIVED2 }; Type get_type() const { return type_; } };
If compiler proves exact type:
void foo() { Derived d; Base* b = &d; // Compiler knows b points to Derived Derived* d2 = dynamic_cast<Derived*>(b); // Optimized to: d2 = (Derived*)b; (no RTTI check!) }
Check optimization:
clang++ -O2 -S -mllvm -print-after-all test.cpp 2>&1 | grep dynamic_cast # If output is empty, dynamic_cast was elided
#include <cstdio> #include <typeinfo> void print_layout(void* obj) { void** vptr = *(void***)obj; printf("Object at: %p\n", obj); printf("VPtr: %p\n", vptr); const std::type_info* ti = (std::type_info*)vptr[-1]; printf("Type: %s\n", ti->name()); ptrdiff_t* offset = (ptrdiff_t*)(vptr - 2); printf("Offset-to-top: %ld\n", *offset); } int main() { Derived d; print_layout(&d); Interface* i = &d; print_layout(i); }
Output:
Object at: 0x7ffc1234
VPtr: 0x55555678
Type: 7Derived
Offset-to-top: 0
Object at: 0x7ffc1244
VPtr: 0x55555788
Type: 7Derived
Offset-to-top: -16
(gdb) p *(void***)obj # Print vptr (gdb) p **(void***)obj@5 # Print first 5 vtable entries (gdb) p *(long*)(**(void***)obj - 2) # offset-to-top (gdb) p *(char**)(**(void***)obj - 1) # type_info name
#include <cassert> Derived* d = new Derived(); Base* b = d; Interface* i = d; // These should all succeed Derived* d1 = dynamic_cast<Derived*>(b); assert(d1 == d); Derived* d2 = dynamic_cast<Derived*>(i); assert(d2 == d); Interface* i2 = dynamic_cast<Interface*>(b); assert(i2 == i); // Address checks assert((char*)i == (char*)d + 16); // Interface is 16 bytes offset
RTTI enables dynamic_cast via:
Key memory patterns:
Cast types:
Cost: ~100 cycles, avoid in hot loops.
Questions on specific aspects?
It decides which problems come first, which drills open, which labs you see and how the Learn surface is organised. Change it any time from the switch in the top bar, or press Escape to look around first.