Skip to Content
Course content

225: Understanding vtables and Dynamic Dispatch Internals

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've spent a lot of time reviewing code from developers who understand how to use polymorphism, but have a hazy idea of what's actually happening under the hood. When it comes to virtual functions, there is one misconception that keeps popping up: the idea that the object instance somehow "carries" the logic for its overridden functions, or that the compiler creates a complex search-and-find operation every time you call a virtual method.

If you believe that calling a virtual function is like doing a dictionary lookup—searching through a list of names to find the right function—you're imagining a performance hit that simply isn't there. C++ doesn't do string lookups at runtime. That would be catastrophically slow.

The Myth: Virtual Functions Bloat Every Object Instance

A common worry is that adding more virtual functions to a base class increases the size of every single object created from that class. The logic usually goes: "If I have ten virtual functions, the object must be storing ten pointers to those functions."

struct Entity {
    virtual void update() {}
    virtual void render() {}
    virtual void onCollision() {}
    // Does this object now contain 3 pointers?
};

Let's kill that myth right now. If you run sizeof(Entity) on a 64-bit system, you'll see it's 8 bytes. Not 24. Not 32. Just 8. Why? Because the object doesn't store the functions. It stores exactly one thing: a pointer to a table.

The Reality: The vptr and the Shared vtable

Here is how it actually works. For every class that has at least one virtual function, the compiler creates a vtable (virtual method table). This is a static array of function pointers, and there is only one vtable per class, regardless of how many instances you create.

When you instantiate an object, the compiler inserts a hidden pointer called the vptr (virtual pointer) into the object's memory layout. This vptr points to the vtable associated with that specific class. I like to think of the vptr as a "class identity card." It doesn't tell the object what to do; it tells the object where to look up the instructions for its type.

Consider this hierarchy:

class Entity {
    virtual void update() { /* Base logic */ }
};

class Player : public Entity {
    void update() override { /* Player logic */ }
};

class NPC : public Entity {
    void update() override { /* NPC logic */ }
};

In memory, you have one vtable for Entity, one for Player, and one for NPC. Every Player object you spawn has a vptr pointing to the Player vtable. Every NPC points to the NPC vtable. The size of the object remains constant regardless of how many virtual functions you add to the class definition.

Tracing the Pointer Hop: How the CPU Actually Finds the Code

So, what happens when you write entityPtr->update()? If entityPtr is a base class pointer pointing to a Player object, the CPU doesn't "search" for the Player version of the function. It follows a very rigid, fast path of indirection:

  • Step 1: Follow entityPtr to the object in memory.
  • Step 2: Read the vptr located at the start of that object.
  • Step 3: Offset into the vtable to find the slot for update() (the compiler knows update() is always at index 0).
  • Step 4: Jump to the function address stored in that slot.

That's it. It's just a few pointer dereferences. This is why virtual functions have a slight overhead compared to regular functions, but it's a constant-time operation. It doesn't matter if your inheritance tree is two levels deep or twenty; the "hop" is always the same distance.

One detail I should mention: this is also why you cannot call virtual functions safely in a constructor. When the Entity constructor is running, the object is still an Entity; the vptr hasn't been updated to point to the Player vtable yet. If you call a virtual function there, you'll hit the base class implementation, even if you're in the middle of constructing a derived object. It's a classic foot-gun that catches even experienced devs.




📋 Practical Task

Exercise: Simulating a Manual Dispatch Table

To truly internalize how vtables work, you're going to build a "manual" version of dynamic dispatch. Instead of using the virtual keyword, you will implement a system that mimics the vptr/vtable mechanism using function pointers.

Requirements:

  • Create a BaseEntity struct that contains a pointer to a VTable struct.
  • The VTable struct should contain function pointers for update() and render().
  • Implement two derived-like structs, PlayerEntity and EnemyEntity. Each should have its own static VTable instance.
  • In the constructor of the entities, manually assign the BaseEntity's vtable pointer to the corresponding static vtable.
  • Write a function void executeUpdate(BaseEntity* entity) that triggers the update function by following the vtable pointer.

Goal: Prove that you can achieve polymorphic behavior (different outputs for different objects) using a single base pointer, without using the C++ virtual keyword.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.