Polymorphism & Virtual Functions
A virtual function lets a call through a base pointer/reference dispatch to the derived override at runtime — the mechanism behind dynamic polymorphism.
Why it matters
Runtime polymorphism is how one loop over std::vector<std::unique_ptr<Shape>> draws circles and squares without knowing their types — the open-closed principle in action. The cost (an indirect call through a vtable, ~1–3 ns and an inhibited inline) is the trade you accept for that flexibility; knowing when it is and isn’t paid for is core to writing fast C++.
How it works
A class with any virtual function gets a hidden vptr pointing to a per-class vtable of function addresses. A virtual call loads the vptr, indexes the vtable, and calls — dynamic dispatch.
- Dispatch is dynamic only through a pointer or reference; calling through an object (
obj.f()) or a sliced value is resolved statically. - Mark every intended override
overrideso a signature mismatch (e.g. a strayconst) is a compile error instead of a silent new function. - A polymorphic base needs a
virtualdestructor, ordelete basePtrskips the derived destructor — UB / leak. finalon an override stops further overriding and may let the compiler devirtualize; non-polymorphic dispatch (templates/CRTP) avoids the vtable entirely.
Example
struct Shape { virtual double area() const = 0; virtual ~Shape() = default; };
struct Circle : Shape {
double r;
double area() const override { return 3.14159 * r * r; }
};
void print(const Shape& s) { std::cout << s.area(); } // dispatches to Circle
print(Circle{2.0}); // 12.566 — chosen at runtime via the vtableThe same print works for any future Shape subclass without recompilation of print.
Pitfalls
- Missing
virtual ~Base(): deleting a derived object through aBase*runs only the base destructor — leaks/UB. - Slicing (passing a derived by value as
Base) discards the dynamic type; dispatch reverts toBase. - Calling a virtual from a ctor/dtor uses the current class’s override, not the derived one — the object isn’t whole yet.
- Forgetting
overridelets a subtly different signature create a new function that never gets called; always annotate.