CRTP — Curiously Recurring Template Pattern in C++: elegant static polymorphism
How CRTP works, when to use it, policy/mixin patterns, C++20 improvements, pitfalls, and practical examples you can compile and run.
The Curiously Recurring Template Pattern (CRTP) is a simple, powerful pattern in C++ that gives you polymorphic-like behavior at compile time. Use CRTP when you need the extensibility and reuse of inheritance but want to avoid virtual calls and vtables for performance or compile-time interface validation. It's particularly useful for mixins, policy-based design, and zero-overhead abstractions.
Edge cases to consider: object slicing, open hierarchies (where runtime polymorphism is required), heterogeneous containers of different CRTP types (requires wrappers), and ABI stability.
CRTP is when a class template uses a derived class as a template parameter. The derived class then inherits from the template instantiation. The result looks circular — hence the "curiously recurring" name — but is perfectly legal in C++ and powerful.
Minimal form:
1template <typename Derived>
2struct Base {
3 void interface() {
4 // static polymorphism: call derived implementation
5 static_cast<Derived*>(this)->implementation();
6 }
7};
8
9struct Derived : Base<Derived> {
10 void implementation() { /* ... */ }
11};
12Base<T> can call methods on T via static_cast<T*>(this). Calls are resolved at compile time with no vtable.
Use CRTP when:
Use virtual functions when:
Trade-offs: CRTP offers zero-overhead and compile-time checks, but eliminates runtime flexibility.
A practical example: a small logging mixin that provides log() but calls a format() provided by the derived type.
1#include <iostream>
2#include <string>
3
4template<typename Derived>
5struct Logger {
6 void log(const std::string& msg) {
7 std::cout << static_cast<Derived*>(this)->format(msg) << '\n';
8 }
9};
10
11struct Simple : Logger<Simple> {
12 std::string format(const std::string& msg) { return "[Simple] " + msg; }
13};
14
15int main() {
16 Simple s;
17 s.log("hello");
18}
19Compile and run:
1# g++ -std=c++20 -O2 -Wall crtp_logger.cpp -o crtp_logger && ./crtp_logger
2Because log calls format via static_cast<Derived*>(this), the call is resolved statically — no virtual table nor runtime dispatch.
CRTP shines for mixins: small behaviors you can "mix in" to types without repeating code.
Example: add equality comparable behavior using a derived equals method.
1template <typename Derived>
2struct EqualityComparable {
3 friend bool operator==(const Derived& a, const Derived& b) {
4 return a.equals(b);
5 }
6 friend bool operator!=(const Derived& a, const Derived& b) {
7 return !operator==(a, b);
8 }
9};
10
11struct Point : EqualityComparable<Point> {
12 int x, y;
13 bool equals(const Point& o) const { return x == o.x && y == o.y; }
14};
15Policy-based design: make behavior configurable via template parameters.
1struct AllocatorPolicyDefault { /* ... */ };
2struct AllocatorPolicyCustom { /* ... */ };
3
4template <typename Derived, typename AllocPolicy = AllocatorPolicyDefault>
5class ContainerBase : public AllocPolicy {
6 // container implementation that depends on AllocPolicy and uses Derived for specifics
7};
8This lets you compose behavior at compile-time with no runtime cost.
Because CRTP relies on the derived type implementing specific functions, you can provide helpful compile-time diagnostics.
Pre-C++20 SFINAE helper (simple):
1#include <type_traits>
2
3template<typename T>
4using has_serialize_t = decltype(std::declval<T>().serialize());
5
6template<typename Derived>
7struct Serializer {
8 static_assert(std::experimental::is_detected_v<has_serialize_t, Derived>,
9 "Derived must implement serialize()");
10};
11C++20 makes this cleaner with concepts:
1template <typename T>
2concept Serializable = requires(T a) {
3 { a.serialize() } -> std::convertible_to<std::string>;
4};
5
6template <Serializable Derived>
7struct SerializerCRTP {
8 std::string save() { return static_cast<Derived*>(this)->serialize(); }
9};
10When the derived type doesn't satisfy Serializable, you get a readable compiler error.
CRTP pairs well with constexpr and consteval to move even more behavior to compile time. Example: a compile-time registry of capabilities.
1#include <array>
2#include <string_view>
3
4template<typename Derived>
5struct Tag {
6 static constexpr std::string_view name() { return Derived::static_name(); }
7};
8
9struct Foo : Tag<Foo> { static constexpr std::string_view static_name() { return "Foo"; } };
10
11static_assert(Foo::name() == "Foo");
12Use consteval factories and if consteval in complex metaprograms to reduce runtime cost.
Base<...> without erasure. Solutions: type erasure wrappers, std::variant, or traditional virtual base pointers.Serialization visitor-like pattern (compile-time visitor): when the set of types is closed or known, you can implement visitor-like behavior using CRTP helper templates instead of virtual visitors.
Example: variant-like static visitor aggregator.
1#include <iostream>
2
3template <typename Derived>
4struct Printer { // CRTP provides a generic print() that calls derived print_impl
5 void print() const { static_cast<const Derived*>(this)->print_impl(); }
6};
7
8struct A : Printer<A> { void print_impl() const { std::cout << "A\n"; } };
9struct B : Printer<B> { void print_impl() const { std::cout << "B\n"; } };
10
11int main() {
12 A a; B b; a.print(); b.print();
13}
14Policy-based allocators, mixins for thread-safety, retry/backoff strategies, and compile-time feature toggles are all common uses.
A recommended minimal test strategy:
1struct VBase { virtual int work() = 0; virtual ~VBase() = default; };
2struct VImpl : VBase { int work() override { return 1; } };
3
4template<typename D> struct CBase { int work() { return static_cast<D*>(this)->work_impl(); } };
5struct CImpl : CBase<CImpl> { int work_impl() { return 1; } };
6
7// measure loop calling vptr vs static call
8When benchmarking, ensure:
Save this file as crtp_logger.cpp and compile:
1// crtp_logger.cpp
2#include <iostream>
3#include <string>
4
5template<typename Derived>
6struct Logger {
7 void log(const std::string& msg) {
8 std::cout << static_cast<Derived*>(this)->format(msg) << '\n';
9 }
10};
11
12struct Simple : Logger<Simple> {
13 std::string format(const std::string& msg) { return "[Simple] " + msg; }
14};
15
16int main() {
17 Simple s;
18 s.log("hello from CRTP");
19}
20Compile and run:
1g++ -std=c++20 -O2 -Wall crtp_logger.cpp -o crtp_logger && ./crtp_logger
2For the equality mixin example, try:
1// crtp_eq.cpp
2#include <iostream>
3
4template <typename Derived>
5struct EqualityComparable {
6 friend bool operator==(const Derived& a, const Derived& b) {
7 return a.equals(b);
8 }
9 friend bool operator!=(const Derived& a, const Derived& b) {
10 return !operator==(a, b);
11 }
12};
13
14struct Point : EqualityComparable<Point> {
15 int x, y;
16 bool equals(const Point& o) const { return x == o.x && y == o.y; }
17};
18
19int main() {
20 Point a{1,2}, b{1,2}, c{2,3};
21 std::cout << std::boolalpha << (a == b) << " " << (a != c) << "\n";
22}
23Compile and run similarly.
Q: Can I store CRTP types in a vector?
A: You can store them by value if all are the same concrete type; for heterogenous types use std::variant or type-erasure wrappers.
Q: Is CRTP faster than virtual calls always?
A: In tight loops yes — CRTP eliminates the indirection. But profile your real workload: branch misprediction, cache effects and inlining can shift numbers.
Q: Does CRTP interact with RTTI?
A: CRTP is a compile-time technique; if you need runtime type information across an inheritance hierarchy you still rely on RTTI/virtuals.
CRTP is a lightweight, zero-cost abstraction pattern that belongs in every modern C++ engineer's toolbox. Use it to:
Prefer clear, small CRTP bases with well-documented expected derived API. When runtime flexibility or ABI stability matters, prefer virtual interfaces or combine CRTP for compile-time parts and virtuals for runtime-erased plumbing.
Technical Writer
NordVarg Engineering Team is a software engineer at NordVarg specializing in high-performance financial systems and type-safe programming.
Get weekly insights on building high-performance financial systems, latest industry trends, and expert tips delivered straight to your inbox.