Polymorphism allows us to write generic interfaces where base class pointers invoke derived class overrides at runtime. In languages like Java, all class methods are polymorphic by default. In C++, polymorphism is opt-in (using the virtual keyword) because runtime dynamic dispatch adds physical memory and execution overhead.
Let's look at how polymorphism is implemented in memory and the critical rule of virtual destructors.
The Virtual Table (vtable) and Virtual Pointer (vptr)
To resolve virtual function calls at runtime, the compiler creates a hidden directory table for each class called the Virtual Table (vtable). The vtable contains function pointers pointing to the actual method implementations.
When you instantiate a class with virtual functions, the compiler silently inserts a hidden pointer member called the Virtual Pointer (vptr) at the very top of your object layout. The vptr points to the class's vtable.
The Cost of Dynamic Dispatch
When you call a virtual function, the CPU cannot jump directly to the instruction block. Instead, it must execute three separate steps:
1. Dereference the object's vptr to find the address of the vtable.
2. Index into the vtable to locate the target function pointer.
3. Perform an indirect call to that address.
This vtable lookup adds latency and prevents the compiler from performing inline optimizations. For this reason, systems programmers in latency critical environments (like high frequency trading or game rendering engines) avoid virtual functions inside loops.
* The Switchboard Metaphor: Calling a standard function is like dialing a direct number. Calling a virtual function is like calling a 1950s switchboard operator. You have to wait for the operator to look up the name in a directory book (vtable) and plug the wire in, adding connection delays to your execution path.
The Critical Hazard: Virtual Destructors
If you delete a derived class object through a base class pointer and the base class does not have a virtual destructor, C++ will only call the base class destructor! The derived class destructor will be bypassed entirely, causing dynamic members in the derived class to leak:
To fix this hazard, always declare destructors as virtual in any class you expect developers to inherit from. This guarantees the destructor chain runs in the correct order: from the derived class up to the base class.