CRTP: how the standard library gets polymorphism without a vtable
#cpp-patterns·from crtp-curiously-recurring-template-pattern
TL;DR: a class template that inherits from a base parameterized on itself gets compile-time (static) polymorphism — same interface-sharing benefit as virtual functions, zero vtable, zero indirect call, full inlining. Cost: no runtime type erasure, so no heterogeneous containers of the base type, and every distinct derived type is its own base instantiation. std::enable_shared_from_this is CRTP, already living in your codebase.
The cost virtual functions don't advertise
A class with any virtual function carries a vtable pointer — one word per object, not per class. Every call through a base pointer is an indirect call: load the vptr, load the slot, jump. The compiler can't inline it (usually — devirtualization exists but is fragile across translation units), can't reorder around it as freely, and the indirection is a branch target the CPU has to predict. None of this matters for a function called a few times a second. It matters a great deal in a loop that calls it ten million times, which is exactly the shape of code in a physics step, a parser's inner dispatch, or a hot serialization path.
CRTP exists to buy the same "shared interface, per-type behavior" shape virtual functions give you, resolved entirely at compile time.
The mechanism
The base class is a template. The derived class passes itself as the template argument:
template <typename Derived>
class Shape {
public:
double area() const {
return static_cast<const Derived*>(this)->area_impl();
}
};
class Circle : public Shape<Circle> {
public:
explicit Circle(double r) : r_(r) {}
double area_impl() const { return 3.14159265 * r_ * r_; }
private:
double r_;
};
Nothing here is dynamic. Shape<Circle>::area() is a distinct, fully-typed function — the compiler knows at the call site that this is a Circle, so the static_cast is a no-op and area_impl() is an ordinary, inlinable function call. Compile Shape<Circle>::area() once, specialized for Circle; compile a different one, specialized for Square, if you have one. There is no shared, single, runtime-dispatched area() — there never was one to share.
It's already in the standard library
std::enable_shared_from_this<T> is the canonical example living in every C++ codebase that touches shared_ptr. You write class Widget : public std::enable_shared_from_this<Widget>, and the base — parameterized on your exact derived type — can hand back a correctly-typed shared_ptr<Widget> from inside a member function via shared_from_this(), without ever needing a virtual function or knowing about Widget at the point the base was written. That's the whole pattern, already load-bearing in code you've probably called without noticing it was CRTP.
The pattern that actually earns its keep: mixins
The more common real use isn't performance-critical shape hierarchies — it's giving a type a family of related operations for free, from one written definition. The classic example is comparison operators: define operator== once on your type, get !=, <, >, <=, >= derived from it and one more, with no runtime cost and no repetition per type:
template <typename Derived>
class EqualityComparable {
public:
friend bool operator!=(const Derived& a, const Derived& b) {
return !(a == b);
}
};
class Point : public EqualityComparable<Point> {
public:
int x, y;
friend bool operator==(const Point& a, const Point& b) {
return a.x == b.x && a.y == b.y;
}
};
// Point now has operator!= for free, generated at compile time,
// with zero runtime indirection and no vtable added to Point.
Stack a few of these — EqualityComparable, Printable, Hashable — and you get a la carte, compile-time-composed capability without multiple inheritance from unrelated abstract interfaces and without a single virtual call in sight. This is the same idea C++20 concepts and things like <=> partially subsume for comparisons specifically, but the general mixin technique still applies to anything you want to bolt onto a type mechanically.
Where it bites you
- No heterogeneous containers.
Shape<Circle> and Shape<Square> are unrelated types — there is no common non-template base, so std::vector<Shape*> simply doesn't type-check. If you genuinely need runtime polymorphism — a container holding different concrete types through one pointer type — CRTP is the wrong tool full stop; that's what virtual functions are for. Some codebases combine both: a thin abstract interface for the heterogeneous-storage case, with CRTP underneath to generate the boilerplate implementation for each concrete type.
- The broken-invariant footgun. Nothing stops you from writing
class Square : public Shape<Circle> — a copy-paste error that compiles cleanly and silently gives Square the wrong base specialization. The curious part of "curiously recurring" is exactly this: the pattern only works if the derived class faithfully passes itself, and the language doesn't enforce that for you.
- Code bloat. Every distinct
Derived is a full, separate instantiation of the base template — same tradeoff as any C++ template. Ten shape types sharing one virtual base means one area() in the binary; ten shape types via CRTP means ten, each inlined into its callers, each contributing to compile time and object file size. That's the whole point when you want the inlining — but it's a cost you're deliberately signing up for, worth checking against actual binary-size budgets on constrained targets.
- Debuggability. A stack trace through a chain of CRTP mixins is a stack trace through template-instantiated names — verbose, and less immediately legible than a clean virtual call chain when you're staring at a crash dump at 2am.
When to actually reach for it
Use it when you know at compile time which concrete type you're working with and the interface-sharing is the only thing you wanted from inheritance — mixins, static-dispatch hot loops, library code like enable_shared_from_this that needs to hand back a correctly-typed handle. Don't use it as a default replacement for virtual functions; reach for CRTP when profiling (or the shape of the problem) tells you the indirection actually costs something, not preemptively. Virtual dispatch exists because sometimes you genuinely don't know the concrete type until runtime — in that case CRTP can't help you, because "compile-time polymorphism" is a contradiction in terms for a decision that's inherently made at runtime.