C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
225: Understanding vtables and Dynamic Dispatch Internals
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
entityPtrto 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 knowsupdate()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
BaseEntitystruct that contains a pointer to aVTablestruct. - The
VTablestruct should contain function pointers forupdate()andrender(). - Implement two derived-like structs,
PlayerEntityandEnemyEntity. Each should have its own staticVTableinstance. - 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 theupdatefunction 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.
There are no comments for now.