Week 22 · Phase 3 — The Architect
Polymorphism and virtual functions — one line of code, many behaviours.
Photo · Stephen McFadden / Unsplash
Last week's inheritance gave you a tree of related classes — Circle, Square, Triangle all are Shape. But a tree of types isn't yet useful unless you can mix them. You want a std::vector<Shape*> that can hold any kind of shape, and a single shape->area() call that does the right thing for whichever type's actually there.
That trick is called polymorphism — Greek for "many forms" — and in C++ it costs you exactly one keyword: virtual.
class Shape {
public:
double area() const { return 0; } // not virtual
};
class Circle : public Shape {
public:
double area() const { return 3.14 * r_ * r_; }
private:
double r_ = 5;
};
Shape *s = new Circle();
std::cout << s->area(); // prints 0 — Shape's version!
That's almost certainly not what you wanted. The pointer is typed Shape* (because we wanted to handle many kinds of shape uniformly), and C++ called Shape::area() based on the declared type, not the actual type sitting in memory. This is called static dispatch — the compiler picks the function based on the type at compile time.
virtualclass Shape {
public:
virtual double area() const { return 0; }
virtual ~Shape() = default; // (about destructors next week)
};
class Circle : public Shape {
public:
double area() const override { return 3.14 * r_ * r_; }
private:
double r_ = 5;
};
Shape *s = new Circle();
std::cout << s->area(); // 78.5 — Circle's version, correctly
Two new keywords:
virtual on the base — "this method can be overridden, and at runtime, dispatch to the actual type's version".override on the derived — "I'm explicitly overriding a virtual method from the base". Optional but always recommended; the compiler will catch typos and method-signature mismatches if you use it.Now s->area() uses dynamic dispatch — the actual type of the object is consulted at runtime to pick the right function. Circle's version runs. If s had been a Square*, that would've run instead.
Every class with at least one virtual method gets a hidden table called the vtable. The vtable is an array of function pointers — one for each virtual method — pointing at the concrete implementation for this specific class. Every object of that class gets, as a hidden first field, a pointer to its class's vtable.
// conceptually:
// Circle's vtable: [ &Circle::area, &Circle::~Circle ]
// Square's vtable: [ &Square::area, &Square::~Square ]
// every Circle obj: [ vtable_ptr -> Circle's vtable ] [ r_ ]
// every Square obj: [ vtable_ptr -> Square's vtable ] [ side_ ]
// s->area() compiles to:
// vtable_ptr = *s // load the vtable address from the object
// func = vtable_ptr[0] // pick the slot for area()
// call func(s) // jump there with `this = s`
Two indirections, one indirect call. Slightly more expensive than a direct call — typically 1–2 extra cycles, plus a possible cache miss on the vtable. For 99% of code this is fine. For the inner loop of a numerical kernel it's catastrophic, which is why those kernels never use virtuals.
Sometimes the base class shouldn't be instantiable on its own — there's no sensible default area() for an abstract Shape. C++ lets you say "every derived class must override this; the base has no body":
class Shape {
public:
virtual double area() const = 0; // "= 0" makes it pure virtual
virtual ~Shape() = default;
};
Shape s; // COMPILE ERROR: can't instantiate abstract class
Shape *p = new Circle(); // fine — Circle implements area()
An abstract class is a contract. "If you want to be a Shape, you must implement area()". It's the C++ equivalent of an "interface" in Java/C#. Most large frameworks define one or two abstract base classes that everyone else implements: Module in PyTorch, QObject in Qt, UIView in UIKit.
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(5));
shapes.push_back(std::make_unique<Square>(3));
shapes.push_back(std::make_unique<Triangle>(4, 3));
double total = 0;
for (const auto& s : shapes) {
total += s->area(); // dispatches correctly to each type
}
This single loop adds up areas for any mix of shapes. The code never names Circle or Square; it just talks to Shape. New shape types can be added later without touching this loop. That is the power of polymorphism. Add a new feature; existing code keeps working.
Virtual dispatch isn't free. It's:
For most code, the cost is invisible. For inner loops of numerical kernels — the matrix multiplies inside an AI model — you do not want a virtual call between every multiplication. That's why low-level numerical libraries are written without virtuals (templates instead — a different polymorphism, resolved at compile time). It's also why "cold" framework code and "hot" computational kernels often look like two different languages, even within a single C++ codebase. They are. Use virtuals where flexibility matters; use templates where speed matters.
PyTorch's nn::Module declares a virtual forward(). Every layer (Linear, Conv2d, LayerNorm) overrides it. When you call my_model(x), you're walking a tree of Modules, and each level does child->forward(input) — virtual dispatch. The model is, structurally, a polymorphic graph.
Inside the layer's forward, though? Pure templates and intrinsics. The chef does not pay vtable cost on every multiplication. The model is polymorphic at the layer boundary, monomorphic inside.
Polymorphism is the spine of every framework. Templates are the muscles inside the kernels.
area() virtual. Put a Circle and a Square in a std::vector<std::unique_ptr<Shape>>. Loop over them, summing areas.virtual on the base. Watch the wrong function get called. Then add it back.override on the derived. Subtly misspell the method name (e.g. aera). Notice that without override the compiler doesn't catch it; with override, it does.Polymorphism makes us think in terms of interfaces rather than concrete types. Pushed all the way, that becomes one of the most useful design ideas in software: program against an abstract interface, not a specific implementation.
Week 23 is Abstraction — the principle of designing systems without committing to how they're built.
Photo free under the Unsplash license. Masks · Stephen McFadden.