Dynamic and static behavioral dispatch in C++: compile-time polymorphism (function & operator overloading) vs runtime polymorphism (virtual functions & late binding), the vtable/vptr memory mechanism, pure virtual functions, abstract base classes (interfaces), and the virtual destructor contract.
Polymorphism ("many forms") enables a single interface to control operations across different underlying types. In C++, polymorphism is cleanly split into two distinct execution strategies:
| Feature | Compile-Time Polymorphism (Static Binding) | Runtime Polymorphism (Dynamic Binding) |
|---|---|---|
| Mechanisms | Function Overloading, Operator Overloading, Templates | Virtual Functions, Method Overriding via Base Pointers |
| Binding Time | Compile-time (Early Binding) | Runtime execution (Late Binding) |
| Resolution Speed | Instantaneous; zero runtime latency (inlined direct jumps) | Slight latency (single pointer dereference through vtable) |
| Memory Cost | Zero per-object memory overhead | 8 bytes per object for vptr + shared class vtable |
| Flexibility | Types must be known at compile time | Allows heterogeneous runtime plugins and collections |
The compiler resolves the exact memory address of the target function during compilation. The generated machine code contains a direct assembly call:
call 0x00401120 ; Direct address jump
The exact function address cannot be determined until runtime because a Base* could point to any derived instance. The compiler issues an indirect lookup:
call [rax + 8] ; Indirect call via vtable
Compile-time polymorphism delivers maximum throughput by eliminating runtime branch predictions and pointer indirection overheads:
#include <iostream>
// 1. Function Overloading: Resolved via Compiler Name Mangling
class MathEngine {
public:
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
int add(int a, int b, int c) { return a + b + c; }
};
// 2. Operator Overloading: Providing native arithmetic syntax for custom types
class Complex {
public:
double real, imag;
Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}
// Overloading the binary '+' operator
Complex operator+(const Complex& other) const {
return Complex(real + other.real, imag + other.imag);
}
void print() const {
std::cout << real << " + " << imag << "i\n";
}
};
int main() {
MathEngine math;
std::cout << "Int add: " << math.add(10, 20) << "\n"; // Calls add(int, int)
std::cout << "Double add: " << math.add(2.5, 4.2) << "\n"; // Calls add(double, double)
Complex c1(3.0, 4.0), c2(1.5, 2.5);
Complex c3 = c1 + c2; // Equivalent to: c1.operator+(c2);
std::cout << "Complex sum: ";
c3.print();
return 0;
}
vtable / vptr Architecture
Prefixing a member function with the virtual keyword instructs the compiler to enable Dynamic Dispatch. When a function is called through a base class pointer or reference, the program resolves the actual derived type of the object at runtime rather than relying on the pointer's static type:
0x7000int derivedData0x004011500x004011800x00401200#include <iostream>
#include <vector>
class BaseRenderer {
public:
// Virtual function enables runtime late-binding
virtual void render() const {
std::cout << "[BaseRenderer] Default render pass.\n";
}
// MANDATORY: Virtual Destructor for Polymorphic Base Classes!
virtual ~BaseRenderer() {
std::cout << "[BaseRenderer] Destroyed.\n";
}
};
class VulkanRenderer : public BaseRenderer {
public:
// C++11 'override' specifier verifies exact matching signature at compile-time
void render() const override {
std::cout << "[VulkanRenderer] Executing hardware SPIR-V pipeline.\n";
}
~VulkanRenderer() override {
std::cout << "[VulkanRenderer] GPU command buffers released.\n";
}
};
class DirectXRenderer : public BaseRenderer {
public:
void render() const override {
std::cout << "[DirectXRenderer] Executing DirectX 12 command queue.\n";
}
~DirectXRenderer() override {
std::cout << "[DirectXRenderer] Direct3D device released.\n";
}
};
int main() {
// Heterogeneous polymorphic collection:
std::vector<BaseRenderer*> pipeline;
pipeline.push_back(new VulkanRenderer());
pipeline.push_back(new DirectXRenderer());
std::cout << "--- Executing Dynamic Dispatch Loop ---\n";
for (const BaseRenderer* engine : pipeline) {
engine->render(); // Dynamically dispatches to Vulkan or DirectX via vtable!
}
std::cout << "\n--- Polymorphic Deallocation ---\n";
for (BaseRenderer* engine : pipeline) {
delete engine; // Correct derived destructor executed due to virtual destructor!
}
return 0;
}
If a class contains at least one virtual function, its destructor MUST be declared virtual (virtual ~Base() {}). Deleting a derived object through a Base* pointer when the base destructor is non-virtual results in Undefined Behavior: the runtime executes only the base class destructor, skipping the derived class destructor entirely and leaking all derived heap allocations!
A Pure Virtual Function is declared with the trailing syntax = 0, signifying that the base class provides no default implementation and mandates that all concrete derived classes must provide an override:
virtual void execute() = 0;
In the class's vtable, the slot for a pure virtual function is populated with a special null or runtime error handler stub (such as __cxa_pure_virtual).
Any class containing at least one pure virtual function is an Abstract Class. The compiler forbids instantiating abstract classes directly:
// Shape s; // COMPILE ERROR! Cannot instantiate abstract class
class ILogger { virtual void log(msg) = 0; };class CloudLogger : public ILoggerlog(msg). If skipped, CloudLogger remains abstract and unusable.const ILogger&, completely decoupled from underlying cloud or file storage mechanics.vtable and vptr?
vtable (Virtual Method Table) is a static array of function pointers constructed once per class at compile-time for any class declaring or inheriting virtual functions. Each object instance of such a class contains an invisible pointer called the vptr (usually at offset 0), initialized by the constructor to point to that class's specific vtable. When a virtual function is invoked through a base pointer, the CPU dereferences vptr, indexes into the table, and calls the resolved function pointer in late-binding fashion.
delete basePtr resolves via static early-binding to the base destructor only. The derived class destructor is completely skipped, leaving derived heap buffers, file descriptors, or open mutexes unfreed. Making the destructor virtual ensures that late-binding dynamic dispatch invokes the derived destructor first, followed safely by the base destructor.
override specifier accomplish, and why is it best practice?
override instructs the compiler to verify that a member function is indeed overriding an identical virtual function in an ancestor base class. If there is a subtle typo in the function name, a mismatched parameter type, or a missing const qualifier, the compiler halts with an explicit error instead of silently creating a new, separate overload that fails to participate in dynamic dispatch.
call address). A virtual call requires: (1) loading the object's vptr from memory, (2) indexing into the vtable array, and (3) performing an indirect jump (call [reg + offset]). This introduces memory latency, potential CPU L1 cache misses if the vtable is not cached, and prevents the compiler from performing aggressive inline optimization.
final keyword do when applied to a virtual function or class?
virtual void render() final;), it prevents any subsequent derived classes from overriding that function. When applied to a class (class Engine final { ... };), it prevents the class from being inherited altogether. It also allows the compiler to perform devirtualization, converting virtual calls into direct, inlinable static calls when the concrete type is known.
add(int, int) becomes _Z3addii). Because the mangled symbol names are unique, the linker binds calls to their exact matching implementations statically.
= 0) have a function body definition in C++?
void Base::clean() { ... }). Derived classes must still explicitly override the function, but can invoke the base implementation explicitly via Base::clean();. This is frequently used for pure virtual destructors (virtual ~Base() = 0;), which must have a body to prevent linker errors during stack unwinding.