Step 8 of 9
CRTP and static polymorphism
Virtual functions choose which code to run at run time, through a hidden pointer to a table of functions. That's flexible, but every call is an indirect jump the compiler usually can't inline. When the concrete type is known at compile time anyway, the Curiously Recurring Template Pattern (CRTP) gets the same "the base class calls the derived class's version" effect with ordinary, inlinable calls. The name comes from its odd shape: a class inherits from a template instantiated with itself.
#include <iostream>
#include <string>
template <typename Derived>
struct Greeter {
void greet() const {
// The base knows its real type at compile time, so no virtual call is needed.
const auto& self = static_cast<const Derived&>(*this);
std::cout << "Hello from " << self.name() << "\n";
}
};
struct London : Greeter<London> {
std::string name() const { return "London"; }
};
struct Madrid : Greeter<Madrid> {
std::string name() const { return "Madrid"; }
};
template <typename T>
void welcome(const Greeter<T>& g) { g.greet(); }
int main() {
welcome(London{});
welcome(Madrid{});
}
Hello from London
Hello from Madrid
How it works
struct London : Greeter<London>hands the base class the derived type as a template argument. InsideGreeter<London>,DerivedisLondon, so thestatic_castis safe andself.name()is a direct call.- No
virtual, no vtable, no pointer to follow. The compiler sees the exact function and can inline it. - The price:
Greeter<London>andGreeter<Madrid>are unrelated types. There's no common base, so you can't put a London and a Madrid in onestd::vectorand loop over them. Code that uses them must itself be a template, likewelcome. When you need a mixed collection chosen at run time, virtual functions (or type erasure, in the design patterns module) are the tool.
What CRTP is used for
- Mixins: write a feature once in a base template and add it to any class, like comparison operators generated from one
<, or cloning, or printing. - Per-type bookkeeping: static data in a CRTP base is separate for every derived class, because every
Base<X>is a different class. That's the exercise. - Static interfaces in performance-critical libraries (math, graphics, simulations), where virtual calls in tight loops would cost too much.
C++23 added "deducing this" (void greet(this const auto& self)), which covers many of these uses without the curious inheritance. You'll still see CRTP everywhere in existing code.
Your turn: write a CRTP base Counted<T> that tracks how many objects of each derived type are alive. Its constructor and copy constructor add one to a counter, its destructor subtracts one, and static int alive() returns the count. Because the counter lives in Counted<Enemy> and Counted<Bullet> separately, each type gets its own count.
Previous: SFINAE and enable_if Next: Challenge: compile-time computation