#065 – Static polymorphism via the Curiously Recurring Template Pattern (CRTP)

Virtual functions are how C++ usually does polymorphism, and the mechanism is well understood: each polymorphic object carries a hidden pointer to a vtable, and a virtual call is compiled into an indirect jump through that table. The cost is one extra indirection per call and one pointer of overhead per object — small in isolation, real when the call is in a hot loop or the object is one of millions. The Curiously Recurring Template Pattern (CRTP) is the C++ idiom for getting polymorphic behaviour with none of the runtime cost: the dispatch happens at compile time instead.

Three things to take away:

  • CRTP is the shape class Derived : public Base<Derived>. The base class is parameterised on the derived class itself.
  • Inside the base, static_cast<Derived*>(this) recovers the derived type with no runtime check, because the relationship is fixed at compile time.
  • CRTP gives you static polymorphism — zero vtable, zero virtual call — at the cost of losing heterogeneous containers of the base type.

What virtual dispatch actually costs

The standard reference describes the mechanism plainly: each class with at least one virtual function gets a hidden vtable pointer, and every virtual call is compiled into an index into that table followed by a call through the function pointer at that index. The compiler is permitted to skip the indirection only when “the static type and dynamic type always match” — which, for a value called by name, it can usually prove.

Through a base pointer or reference, the compiler cannot prove that, so it emits the indirect call. For most code this is fine. The pattern below exists for the cases where it isn’t: tight numerical kernels, container internals, performance-critical libraries where virtual dispatch is on the wrong side of the cost budget.

The CRTP shape

CRTP is a base class parameterised on the derived class:

template <typename Derived>
class Shape {
public:
    double area() const{
        return static_cast<const Derived*>(this)->areaImpl();
    }
};

class Circle : public Shape<Circle> {
public:
    Circle(double r) : m_radius{ r } {}
    double areaImpl() const{ return 3.14159 * m_radius * m_radius; }
private:
    double m_radius;
};

class Square : public Shape<Square> {
public:
    Square(double s) : m_side{ s } {}
    double areaImpl() const{ return m_side * m_side; }
private:
    double m_side;
};

The base’s area() calls areaImpl() on this, but does so after casting this to const Derived*. There is no virtual keyword anywhere. The cast is safe because Shape<Circle> is the unique base of Circle — the relationship is established at compile time when the template is instantiated, and there is no possibility of a Shape<Circle>* ever pointing at anything other than a Circle.

A call site looks like this:

Circle c{ 2.0 };
Square s{ 3.0 };
std::cout << c.area() << '\n';   // 12.566...
std::cout << s.area() << '\n';   // 9.0

The compiler sees concrete types throughout. c.area() resolves to Shape<Circle>::area, which inlines into a direct call to Circle::areaImpl, which inlines into the multiplication. There is no vtable, no indirection, no per-object overhead. The shape hierarchy has compiled down to the same code a hand-written Circle::area() and Square::area() would produce.

Why the static_cast is safe

The cast in the base looks like the kind of thing the language usually warns against:

return static_cast<const Derived*>(this)->areaImpl();

In a runtime hierarchy, casting a base pointer to a derived pointer requires dynamic_cast precisely because the relationship is dynamic — the actual object might not be the derived type. In CRTP, the relationship isn’t dynamic. Shape<Circle> is only ever inherited by Circle, by construction: that’s what putting Circle in the template argument means. The compiler knows this, so a static_cast is correct without any runtime check.

This is the load-bearing trick. The base class gets to call derived methods on this, with full type information, without any virtual machinery — because the derived type is part of the base class’s own type.

CRTP for behaviour injection (mixins)

The other common use of CRTP is injecting a chunk of derived functionality from a base. The canonical example is comparison operators — given operator<, you can synthesise the rest:

template <typename Derived>
class Ordered {
public:
    friend bool operator>(const Derived& a, const Derived& b)  { return  b < a; }
    friend bool operator<=(const Derived& a, const Derived& b) { return !(b < a); }
    friend bool operator>=(const Derived& a, const Derived& b) { return !(a < b); }
    friend bool operator!=(const Derived& a, const Derived& b) { return  (a < b) || (b < a); }
};

class Version : public Ordered<Version> {
public:
    Version(int maj, int min) : m_maj{ maj }, m_min{ min } {}
    friend bool operator<(const Version& a, const Version& b) {
        return std::tie(a.m_maj, a.m_min) < std::tie(b.m_maj, b.m_min);
    }
    friend bool operator==(const Version& a, const Version& b) {
        return a.m_maj == b.m_maj && a.m_min == b.m_min;
    }
private:
    int m_maj, m_min;
};

Version inherits from Ordered<Version> and gets four operators for free, all defined in terms of its own operator<. C++20’s <=> (the spaceship operator) has largely retired this specific use case, but the underlying pattern — base classes that synthesise behaviour by calling back into the derived class — remains useful for things like type-safe enums, pointer arithmetic helpers, and policy-based design.

What CRTP gives up

Static polymorphism is not a free upgrade over runtime polymorphism. You give up two things:

  • No heterogeneous containers of the base type. With a runtime hierarchy, std::vector<Shape*> holds circles and squares side by side. With CRTP, Shape<Circle> and Shape<Square> are different types — there is no common base to point at. If you genuinely need to mix concrete types in one container, CRTP is the wrong tool; reach for virtual functions or std::variant (Nibble #058).
  • Compile-time cost and error-message clarity. Each instantiation generates code; large CRTP hierarchies can bloat binaries and slow builds. Diagnostics from a misuse — calling area() when Derived doesn’t define areaImpl() — surface as template instantiation errors, which are wordier than the “no matching function” you’d get from a virtual hierarchy.

CRTP earns its keep when the polymorphism is genuinely static — when each call site knows the concrete type and you just want shared behaviour without paying for dispatch.

Takeaway

CRTP is C++’s answer to “I want polymorphism, but not at runtime.” A base class parameterised on its own derived class gets to call derived methods through a static_cast, with the relationship fixed at compile time and no virtual machinery generated at all. Reach for it when the call sites know the concrete types and performance pays attention — performance-sensitive numerical code, mixin-style behaviour injection, library internals. Stay with virtual functions and the NVI shape when you need heterogeneous collections or genuinely runtime-decided types. The rule is simple: CRTP trades runtime flexibility for compile-time specialisation, and the trade is worth it whenever the flexibility wasn’t being used.