2Brute2Best
πŸ”₯0
M3.2

How virtual Works: vtable & vptr Memory Layout

Hard High-frequency interview topic Pro Onlyvptr (+8 bytes)vtable DispatchWhy No Virtual CtorVirtual Call in Ctor/Dtor Trap
⚑ 15-Second Mental ModelM3.2 · Instant Intuition
Single-Read Mastery
The Real-World Physical Analogy (Vtable & Virtual Dispatch)

When you add virtual, the compiler slips a hidden business card (_vptr, 8 bytes) into your pocket. Calling speak() reads the card, dials the phone number listed on it (vtable), and reaches the exact Derived method in 2 CPU hops.

Rule to Remember: Virtual dispatch costs exactly 2 memory indirections: load _vptr -> load slot -> call.
3 Golden Facts to Remember
Fact #1
Why is the vptr stored at offset 0 in modern C++ object layouts?

Placing __vptr at offset 0 allows dynamic dispatch to proceed with the fastest possible assembly instructions: the CPU can dereference the object pointer directly (mov (%rdi), %rax) without adding a memory offset. Furthermore, under single inheritance, both Base* and Derived* point to the exact same memory address, making pointer upcasts zero-cost no-ops.

Fact #2
Where in memory does the vtable reside, and how many vtables are created?

The vtable resides in the .rodata (read-only data) segment of the executable binary. Exactly one vtable per polymorphic class is generated by the compiler across the entire program. All 100,000 instances of Derived share the single read-only Derived vtable in memory; only the 8-byte __vptr is replicated per object instance.

Fact #3
How does the compiler implement devirtualization, and why does it matter?

Devirtualization is an optimization where the compiler proves that a virtual function call can only ever target one specific override (e.g. when called on a concrete value object, or when a class/method is marked final). The compiler replaces the two-hop indirect vtable jump with a direct call instruction, enabling inlining, constant propagation, and vectorization.

01

Interviewer's lens

What separates a strong C++ candidate from a textbook answer

Interviewers drill into virtual mechanics to see if you view polymorphism as magic syntax or understand machine-level memory layout:

  • Object Layout: Drawing the hidden 8-byte __vptr at byte offset 0 and explaining how alignment padding expands struct size (e.g., from 4 bytes to 16 bytes).
  • The Two-Hop Dispatch: Tracing how basePtr->fn() compiles down to dereferencing __vptr, indexing the .rodata function pointer array, and jumping to machine instructions.
  • Construction Lifecycles: Proving why virtual constructors are physically impossible and why virtual dispatch is disabled inside constructors and destructors to prevent accessing uninitialized derived fields.
02

Under the hood & memory layout

Hardware & memory representation

Dynamic dispatch is purely mechanical. When a class declares or inherits a virtual function, the compiler coordinates three pieces:

  1. Where __vptr lives (In the Object): The compiler injects a hidden 8-byte pointer (__vptr) at offset 0 of every object instance. If an object has member variables, they are shifted down to start at offset 8.
  2. What .rodata contains (In the Executable Binary): The compiler generates exactly one shared array of function pointers called the vtable (virtual method table) per polymorphic class. It resides in read-only data memory (.rodata), preceded by runtime type information (RTTI at offset -8).
  3. The 2-Hop CPU Call (At the Call Site): Invoking basePtr->speak() translates into two machine-level pointer indirections: (*basePtr->__vptr[slot])(basePtr).
    • Hop 1: The CPU reads basePtr to load the __vptr address (mov (%rdi), %rax).
    • Hop 2: The CPU indexes into the vtable at a fixed slot offset to load the function pointer and jump to it (call *0x10(%rax)), passing basePtr as the hidden this pointer.

Constructor & Destructor Invariant: An object cannot have a virtual constructor because __vptr is written by the constructor prologue. While Base::Base() runs, __vptr points to Base's vtable; only when Derived::Derived() begins does __vptr advance to Derived's vtable. During ~Base(), __vptr rolls back to Base to protect already-destroyed derived memory.

Hidden __vptr (+8 bytes on 64-bit)

Placed at offset 0 of the object. Even an empty class with one virtual function jumps from 1 byte (sizeof(Empty) == 1) to 8 bytes (sizeof(VirtualEmpty) == 8).

Alignment & Tail Padding Overhead

If a struct contains a single 4-byte int, introducing a virtual method adds the 8-byte vptr and 4 bytes of tail padding to satisfy the 8-byte alignment of the pointer, jumping from 4 bytes to 16 bytes ($300%$ increase!).

vtable Array in .rodata

An immutable array of code pointers shared by all instances of the same class. Each virtual function occupies one 8-byte slot determined at compile time.

Two-Hop Dynamic Dispatch Assembly

mov (%rdi), %rax (dereference object to fetch vptr) followed by call *0x10(%rax) (fetch and jump to slot). Prevents compiler inlining unless devirtualized.

03

Core concepts & syntax traps

01

The Hidden __vptr & Memory Alignment Padding (4B vs 16B)

Adding a virtual function silently alters the physical size and memory alignment of your struct:

  1. The Hidden Pointer: On 64-bit systems, the compiler injects an 8-byte pointer (__vptr) at offset 0.
  2. Alignment Requirement: Because the struct now contains an 8-byte pointer, the entire struct's memory alignment requirement rises from 4 bytes to 8 bytes (alignof = 8).
  3. Tail Padding Explosion:
    • A non-virtual struct with one int takes 4 bytes ([4B int]).
    • Adding a virtual destructor inserts the 8-byte __vptr and appends 4 bytes of trailing padding ([8B vptr] [4B int] [4B padding]).
    • Total size jumps from 4 bytes to 16 bytesβ€”a 4Γ— memory bloat that can degrade CPU cache performance when storing millions of objects.
C++23 snippet
struct FlatData {
    int id; // 4 bytes, alignof = 4, sizeof = 4
};

struct PolyData {
    virtual ~PolyData() = default; // introduces 8-byte vptr
    int id;                        // 4 bytes + 4 bytes tail padding!
    // Total: 8 (vptr) + 4 (id) + 4 (padding) = 16 bytes, alignof = 8
};
Interview takeaway:

Adding a virtual function to small data classes introduces an 8-byte vptr plus alignment padding, significantly increasing cache footprint.

02

Dynamic Dispatch Lowering: Two Pointer Hops & Instruction Cache Cost

When you call basePtr->speak(), the compiler executes a two-hop pointer chain:

  1. Hop 1 (Fetch VTable): The CPU reads the first 8 bytes of the object instance to load the __vptr, discovering where the class's vtable lives in .rodata.
  2. Hop 2 (Fetch Method Address): The CPU reads the function pointer at the compile-time slot offset (e.g., vtable[slot]) and executes an indirect branch (call *%rax).

Why This Matters for Performance:

  • No Inlining: Because the target function address is determined at runtime, the compiler cannot inline the method body or propagate constants.
  • Branch Mispredictions: The CPU branch target buffer (BTB) must guess where the jump leads. If heterogeneous objects alternate in a loop, pipeline stalls occur.
C++23 snippet
// What you write in C++:
basePtr->speak();

// How the compiler lowers it under the hood:
using SpeakFn = void (*)(const Base*);
void** vtable = *reinterpret_cast<void***>(basePtr); // Hop 1: fetch vptr
SpeakFn fn = reinterpret_cast<SpeakFn>(vtable[slot_index]); // Hop 2: fetch slot
fn(basePtr); // Indirect call passing this pointer
Interview takeaway:

Dynamic dispatch incurs two pointer dereferences, defeats compiler inlining, and can cause instruction cache misses.

03

Why Constructors Can NEVER Be Virtual

Expecting a constructor to be virtual is a physical chicken-and-egg paradox:

  1. VTable Dispatch Requires a __vptr: To call a virtual function, the CPU must read the __vptr embedded inside the target object.
  2. The Object Does Not Exist Yet: Before the constructor runs, memory contains only raw uninitialized bytes. The constructor's very first job is to stamp the __vptr into the object's memory header.
  3. The Paradox: You cannot use __vptr to look up the constructor when the constructor is the code responsible for creating the __vptr.

Interview Solution: When asked for a 'virtual constructor', use the Virtual Copy Constructor idiom (clone()) or a Factory Pattern.

C++23 snippet
// ILLEGAL IN C++:
// virtual Base(); // Compiler Error: 'virtual' is not permitted on constructors

// IDIOMATIC SOLUTION: Prototype Pattern / Virtual Constructor Idiom
class Shape {
public:
    virtual ~Shape() = default;
    [[nodiscard]] virtual std::unique_ptr<Shape> clone() const = 0; // Virtual copy ctor
};
class Circle : public Shape {
public:
    [[nodiscard]] std::unique_ptr<Shape> clone() const override {
        return std::make_unique<Circle>(*this);
    }
};
Interview takeaway:

Constructors cannot be virtual because the vptr is initialized by the constructor itself; use clone() or factory methods instead.

04

The Constructor & Destructor Virtual Dispatch Trap

Calling a virtual function inside a constructor or destructor is a classic C++ trap:

  1. Construction Sequence: When constructing a Derived object, Base::Base() runs first. At that moment, the compiler sets __vptr to point to Base's vtable. Any virtual call invokes Base's version, never Derived's.
  2. Why C++ Does This: Derived's member variables have not been constructed yet. If C++ allowed calling Derived::foo(), it would attempt to read uninitialized memory and crash.
  3. Destruction Sequence: In reverse, when ~Base() runs, Derived::~Derived() has already finished tearing down derived members. The compiler resets __vptr back to Base's vtable to prevent methods from accessing dead memory.
C++23 snippet
class Base {
public:
    Base() { init(); } // DANGER: Calls Base::init(), NOT Derived::init()!
    virtual void init() { std::println("Base::init"); }
    virtual ~Base() = default;
};
class Derived : public Base {
    std::string resource_;
public:
    Derived() : resource_("Active") {}
    void init() override {
        // If called from Base(), resource_ would be UNINITIALIZED GARBAGE!
        std::println("Derived::init: {}", resource_);
    }
};
Interview takeaway:

Never call virtual functions in constructors or destructors; dynamic dispatch is disabled to protect uninitialized derived members.

05

Pure Virtual Function Call Crash (__cxa_pure_virtual) & Two-Phase Init

If a base constructor invokes a pure virtual method (= 0), your program will crash at runtime:

  1. The Pure Virtual Slot: In Base's vtable, pure virtual slots do not point to real function code. They point to a crash-handler stub named __cxa_pure_virtual().
  2. The Crash: If Base::Base() calls a pure virtual method (directly or indirectly via a helper function), the CPU jumps into __cxa_pure_virtual(), prints pure virtual method called, and calls std::terminate().
  3. The Safe Idiom (Two-Phase Initialization): Keep constructors simple and non-virtual. Perform polymorphic setup inside a separate postInitialize() method wrapped in a static factory function (create()) after the derived object is fully constructed.
C++23 snippet
// TWO-PHASE FACTORY IDIOM:
class Service {
protected:
    Service() = default; // Direct construction restricted
    virtual void postConstruct() = 0; // Safe polymorphic setup
public:
    virtual ~Service() = default;
    template <typename T, typename... Args>
    static std::unique_ptr<T> create(Args&&... args) {
        auto obj = std::make_unique<T>(std::forward<Args>(args)...);
        obj->postConstruct(); // Object is fully constructed: dynamic dispatch works!
        return obj;
    }
};
Interview takeaway:

Use factory methods with two-phase initialization to execute polymorphic setup safely after constructors finish.

Brute2Best Pro Content

Unlock Live Implementation & Interview Scenarios

The C++23 production code, live coding steps, scenario twist solutions, and common UB traps are part of Brute2Best Pro. Module 1 is completely free.

All 440 visual dry runs & algorithms
Interactive step-by-step playback with pointer & array state transitions
52 comprehensive LLD chapters
Interactive UML diagrams, real-world stories (Food Delivery, Trading, RPG), and practice solutions
40 C++ basics labs & hardware memory models
Vtables, byte layouts, assembly insights, and multi-threading race conditions
Production-ready C++23 source code
Modern idioms, clean contracts, time/space complexity derivations, and interview takeaways

First anchor question per pattern, Part 1 LLD & Module 1 C++ basics are 100% Free forever.

Next In Your Mastery Journey

Apply C++ Internals to LLD & Algorithmic Patterns

Full-stack systems engineering