Skip to content
C++
Language
since C++98
Advanced

CRTP — Curiously Recurring Template Pattern

Static polymorphism, mixins, and compile-time interface enforcement via CRTP — zero virtual overhead, resolved entirely at compile time.

CRTP (Curiously Recurring Template Pattern)since C++98

A pattern where a class D publicly inherits from a template base Base<D>, allowing the base to call methods on the derived type through a static_cast, achieving static (compile-time) polymorphism with no virtual dispatch overhead.

Overview

CRTP works because when Derived inherits from Base<Derived>, the base template is instantiated with Derived as its parameter. Inside Base<Derived>, the actual runtime type is always Derived — the static_cast<Derived*>(this) is defined behavior, not a hack. The compiler resolves all dispatch at instantiation time; no vtable is generated, no indirect branch is taken at runtime.

Three principal uses:

  • Static polymorphism — uniform interface over unrelated concrete types without virtual
  • Mixins — inject operators and behaviors into derived classes without multiple virtual inheritance
  • Interface enforcement — guarantee that derived types implement required methods, caught at compile time

CRTP predates C++11 and works from C++98 onward. C++20 Concepts improve the ergonomics of enforcing derived-side requirements. C++23's deducing this (explicit object parameters) supersedes some CRTP patterns entirely, but CRTP remains essential for pre-C++23 codebases and for patterns that Concepts alone cannot replace.

Syntax

cpp
template<typename Derived>
class Base {
public:
    void interface_method() {
        // Downcast is safe: Base<D> is only ever a subobject of D
        static_cast<Derived*>(this)->impl();
    }
protected:
    ~Base() = default;  // non-virtual protected: prevents delete-through-base
};

class Concrete : public Base<Concrete> {
public:
    void impl() { /* ... */ }
};

The static_cast is the critical ingredient. reinterpret_cast would be undefined behavior; the static_cast is valid because Derived contains Base<Derived> as a subobject, so the pointer relationship holds. The cast is entirely free at runtime — the compiler eliminates it.

Examples

Static polymorphism

The canonical use: a uniform interface without vtable cost. This matters when you have template functions that operate on many shape types in tight numerical loops.

cpp
template<typename Derived>
class Shape {
public:
    double area() const {
        return static_cast<const Derived*>(this)->area_impl();
    }
    double perimeter() const {
        return static_cast<const Derived*>(this)->perimeter_impl();
    }
    void describe() const {
        std::println("area={:.4f} perimeter={:.4f}", area(), perimeter());
    }
protected:
    ~Shape() = default;
};

class Circle : public Shape<Circle> {
    double r_;
public:
    explicit Circle(double r) : r_(r) {}
    double area_impl()      const { return std::numbers::pi * r_ * r_; }  // C++20: std::numbers
    double perimeter_impl() const { return 2.0 * std::numbers::pi * r_; }
};

class Rectangle : public Shape<Rectangle> {
    double w_, h_;
public:
    Rectangle(double w, double h) : w_(w), h_(h) {}
    double area_impl()      const { return w_ * h_; }
    double perimeter_impl() const { return 2.0 * (w_ + h_); }
};

// Works for any Shape<T> — resolved at instantiation, zero overhead
template<typename T>
void print_shape(const Shape<T>& s) { s.describe(); }

Circle    c{5.0};
Rectangle r{4.0, 6.0};
print_shape(c);  // area=78.5398 perimeter=31.4159
print_shape(r);  // area=24.0000 perimeter=20.0000

Note that Shape<Circle> and Shape<Rectangle> are distinct, unrelated types. You cannot place both in a std::vector<Shape<??>> — the heterogeneous container problem is the primary structural limitation of CRTP.

Mixin: Barton-Nackman operator injection

Inject a full set of comparison operators from a single user-defined operator<. This is the technique behind the historical Barton-Nackman trick and is the pattern that C++20's operator<=> largely replaces for comparisons — but the general mixin mechanism remains useful far beyond comparisons.

cpp
template<typename Derived>
struct TotallyOrdered {
    friend bool operator==(const Derived& a, const Derived& b) {
        return !(a < b) && !(b < a);
    }
    friend bool operator!=(const Derived& a, const Derived& b) { return !(a == b); }
    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); }
};

struct Celsius : TotallyOrdered<Celsius> {
    double value;
    explicit Celsius(double v) : value(v) {}
    friend bool operator<(const Celsius& a, const Celsius& b) {
        return a.value < b.value;
    }
};

Celsius boiling{100.0}, freezing{0.0};
assert(boiling > freezing);   // derived from < via mixin
assert(freezing != boiling);

Using friend functions defined inside the base template makes argument-dependent lookup work correctly — the operators are found when the compiler looks up names for Celsius arguments.

Per-type instance counting

Each Counter<T> instantiation has its own inline static counter, giving exact per-type counts without any manual wiring:

cpp
template<typename Derived>
class Counter {
public:
    static int live_count() noexcept { return count_; }  // noexcept: C++11
protected:
    Counter()  noexcept { ++count_; }
    Counter(const Counter&) noexcept { ++count_; }
    ~Counter() noexcept { --count_; }
private:
    inline static int count_ = 0;  // inline static: C++17
};

class Connection : public Counter<Connection> { /* ... */ };
class Session    : public Counter<Session>    { /* ... */ };

{
    Connection a, b, c;
    Session    s;
    assert(Connection::live_count() == 3);
    assert(Session::live_count()    == 1);
}
assert(Connection::live_count() == 0);

C++20: enforcing the interface with Concepts

Prior to C++20, a missing impl() in Derived produced a cryptic error at the call site inside the base. With Concepts, you can catch it early and emit a readable diagnostic:

cpp
template<typename T>
concept HasAreaImpl = requires(const T& t) {
    { t.area_impl() } -> std::convertible_to<double>;
};

template<HasAreaImpl Derived>  // C++20
class Shape {
public:
    double area() const {
        return static_cast<const Derived*>(this)->area_impl();
    }
protected:
    ~Shape() = default;
};

The Concept constraint fires at the point of instantiation, naming exactly what is missing.

C++23: deducing this as an alternative

C++23 introduces explicit object parameters, which collapse many CRTP patterns into simpler non-template base classes:

cpp
// C++23 — no CRTP needed for this pattern
struct Printable {
    void print(this auto const& self) {   // C++23: explicit object parameter
        self.print_impl();
    }
};

struct Widget : Printable {
    void print_impl() const { std::println("Widget"); }
};

Widget w;
w.print();  // resolves Widget::print_impl() without CRTP

Deducing this covers the mixin and static polymorphism cases but does not handle patterns that require the base to name the derived type explicitly (e.g., returning Derived by value, per-type static storage, or template metaprogramming that inspects Derived). CRTP remains necessary for those.

Best Practices

Protect the destructor. If users should not delete through a base pointer, make the destructor protected (as shown above). Only add a virtual destructor to the base if you deliberately want runtime-polymorphic deletion — which defeats the purpose of CRTP.

Use friend free functions for operators. Defining operators as friend inside the template body participates correctly in ADL and avoids implicit conversions on the wrong argument.

Enforce with Concepts in C++20+. Replace silent hard-to-diagnose instantiation errors with Concept constraints on Derived. This also serves as machine-readable documentation of what the base requires.

Prefer deducing this for new code in C++23+. For pure mixin injection where the base does not need to name the derived type, deducing this is simpler, avoids the repetitive : Base<Self> boilerplate, and supports chains of mixins without diamond-inheritance concerns.

Keep the base thin. CRTP bases are not interfaces in the OOP sense — they are behavior injectors. Mixing large amounts of state into a CRTP base complicates the object layout and makes the pattern harder to reason about.

Common Pitfalls

Heterogeneous containers are impossible

Shape<Circle> and Shape<Rectangle> share no common base. You cannot store them together:

cpp
// Does not compile — no common type
std::vector<Shape<?>*> shapes;

// Solutions:
// 1. Add a separate abstract base with virtual dispatch
// 2. std::variant<Circle, Rectangle>  (C++17)
// 3. Type-erased wrapper (see: type erasure idiom)

Calling base methods without the cast

A common mistake is forgetting the cast and accidentally calling the base version:

cpp
template<typename Derived>
struct Base {
    void process() {
        do_work();                              // calls Base::do_work() — wrong
        static_cast<Derived*>(this)->do_work(); // correct
    }
    void do_work() { /* base fallback */ }
};

If Derived::do_work() is non-virtual and the cast is omitted, the base version runs silently. There is no compiler warning.

The incomplete-type trap during instantiation

The derived class is incomplete at the point of inheritance, which means the base constructor cannot call derived methods. The static_cast is only safe in non-constructor member functions:

cpp
template<typename D>
struct Base {
    Base() {
        // UNDEFINED BEHAVIOR — D is incomplete at construction time
        static_cast<D*>(this)->init();
    }
};

Move any initialization that requires the fully-constructed Derived into a separate init() call or a factory function.

CRTP for static interface enforcement

CRTP can make "pure virtual" requirements a compile-time contract. Rather than relying on a runtime = 0 function that simply omits a vtable entry, you can require that Derived exposes exactly the methods the base needs — and fail early with a clear error.

cpp
// C++17 — static_assert inside the base constructor to enforce the interface
template<typename Derived>
class Serializable {
public:
    std::string serialize() const {
        static_assert(
            requires(const Derived& d) { d.to_string(); },  // C++20 ad-hoc requires
            "Derived must implement to_string() const"
        );
        return static_cast<const Derived*>(this)->to_string();
    }
protected:
    ~Serializable() = default;
};

With C++20 Concepts the enforcement moves to the class template parameter, so the diagnostic appears at the point of instantiation rather than deep inside the base:

cpp
// C++20
template<typename T>
concept Stringifiable = requires(const T& t) {
    { t.to_string() } -> std::convertible_to<std::string>;
};

template<Stringifiable Derived>
class Serializable {
public:
    std::string serialize() const {
        return static_cast<const Derived*>(this)->to_string();
    }
protected:
    ~Serializable() = default;
};

struct Good : Serializable<Good> {
    std::string to_string() const { return "Good"; }
};

// struct Bad : Serializable<Bad> {};  // C++20: hard error at template argument

Mixin pattern via CRTP

CRTP mixins inject reusable behavior into any class that opts in, without touching the class hierarchy. Each mixin is a separate base template; a derived class simply inherits from multiple mixins simultaneously.

cpp
// C++11 — composable mixin bases
template<typename Derived>
struct Printable {
    void print() const {
        std::cout << static_cast<const Derived*>(this)->to_string() << '\n';
    }
};

template<typename Derived>
struct Hashable {
    std::size_t hash() const noexcept {
        auto val = static_cast<const Derived*>(this)->hash_value();
        return std::hash<decltype(val)>{}(val);
    }
};

// Derive from both — no virtual overhead, no diamond ambiguity
struct Tag : Printable<Tag>, Hashable<Tag> {
    std::string name;
    explicit Tag(std::string n) : name(std::move(n)) {}

    std::string to_string()  const { return name; }
    std::string hash_value() const { return name; }
};

Tag t{"release"};
t.print();          // release
auto h = t.hash();  // std::hash<std::string>{}("release")

Each mixin base is instantiated independently, so there is no shared state between them — just compiled-in method forwarding.

CRTP vs C++20 concepts

CRTP and Concepts solve overlapping but distinct problems. CRTP provides behavior injection and static dispatch at runtime; Concepts constrain template parameters at instantiation time and document requirements in machine-readable form. The two compose well together.

ConcernCRTP aloneConcepts aloneCRTP + Concepts
Inject behavioryesnoyes
Constrain template argnoyesyes
Readable error on violationpoor (pre-C++20)yesyes
Zero overheadyesyesyes
Requires C++ versionC++98C++20C++20
cpp
// C++20 — concept documents the requirement; CRTP fulfills it
template<typename T>
concept CRTPBase = requires(T t) {
    // every CRTP-derived type must be default-constructible
    // and expose area_impl()
    requires std::default_initializable<T>;
    { t.area_impl() } -> std::floating_point;
};

template<CRTPBase Derived>
struct Shape {
    double area() const {
        return static_cast<const Derived*>(this)->area_impl();
    }
protected:
    ~Shape() = default;
};

The Concept documents the interface contract; CRTP dispatches the implementation. Together they make the pattern self-documenting and immediately diagnosable.

CRTP and operator injection

A CRTP base is the canonical way to synthesize a full operator set from a minimal user-supplied primitive. The standard library itself uses this pattern — std::rel_ops (deprecated in C++20) was a non-CRTP version; std::totally_ordered Concept is the modern successor — but the CRTP form is still common in embedded and numerics code.

cpp
// C++11 — inject all arithmetic operators from += only
template<typename Derived>
struct AdditiveGroup {
    friend Derived operator+(Derived lhs, const Derived& rhs) { return lhs += rhs; }
    friend Derived operator-(Derived lhs, const Derived& rhs) { return lhs -= rhs; }
    friend Derived operator-(Derived val) {
        // unary minus via scaled subtraction
        return Derived{} - val;
    }
};

struct Vec2 : AdditiveGroup<Vec2> {
    float x, y;
    Vec2(float x = 0, float y = 0) : x(x), y(y) {}
    Vec2& operator+=(const Vec2& o) { x += o.x; y += o.y; return *this; }
    Vec2& operator-=(const Vec2& o) { x -= o.x; y -= o.y; return *this; }
};

Vec2 a{1, 2}, b{3, 4};
Vec2 c = a + b;  // {4, 6} — injected from AdditiveGroup
Vec2 d = c - a;  // {3, 4} — injected from AdditiveGroup

The friend free functions are defined inside the base template body, which means ADL finds them when either argument is of type Derived.

Deducing this (C++23) as a CRTP replacement

C++23 explicit object parameters (deducing this) eliminate most CRTP boilerplate for the mixin and static-dispatch patterns. The base class becomes a plain struct and its methods take a this auto first parameter that deduces to the actual derived type.

cpp
// C++23 — CRTP replaced by explicit object parameter
struct Printable {
    void print(this const auto& self) {
        // 'self' deduces to the most-derived type — no static_cast needed
        std::println("{}", self.to_string());
    }
};

struct Hashable {
    std::size_t hash(this const auto& self) noexcept {
        return std::hash<std::string>{}(self.hash_key());
    }
};

struct Tag : Printable, Hashable {    // no <Tag> parameter — simpler
    std::string name;
    explicit Tag(std::string n) : name(std::move(n)) {}
    std::string to_string() const  { return name; }
    std::string hash_key()  const  { return name; }
};

Tag t{"v1.0"};
t.print();        // v1.0
auto h = t.hash();

Deducing this does not replace all CRTP uses. It cannot express per-type static storage (each Base<Derived> is its own instantiation with its own statics), cannot return Derived by value from the base, and cannot be used in contexts where the derived type must appear as a template argument to another template.

CRTP with multiple base policies

CRTP composes cleanly when a class needs behavior from several orthogonal policy bases. Each policy is a separate CRTP template; the derived class inherits them all. The linker sees only the methods actually called, so unused policies cost nothing.

cpp
// C++17 — three independent CRTP policy bases
template<typename D> struct Loggable {
    void log(std::string_view msg) const {
        std::println("[{}] {}", static_cast<const D*>(this)->class_name(), msg);
    }
};

template<typename D> struct Cloneable {
    [[nodiscard]] D clone() const {   // C++17: [[nodiscard]]
        return *static_cast<const D*>(this);
    }
};

template<typename D> struct Comparable {
    friend bool operator==(const D& a, const D& b) { return a.key() == b.key(); }
    friend bool operator< (const D& a, const D& b) { return a.key() <  b.key(); }
};

struct Record
    : Loggable<Record>
    , Cloneable<Record>
    , Comparable<Record>
{
    int id;
    explicit Record(int i) : id(i) {}
    std::string_view class_name() const { return "Record"; }
    int              key()        const { return id; }
};

Record r{42};
r.log("created");                // [Record] created
Record r2 = r.clone();           // copy via Cloneable
assert(r == r2);                 // via Comparable
assert(!(r < r2));

Keep each policy base focused on a single responsibility; the derived class should only need to supply a small set of primitive operations that all policies share.

Pitfall: accessing incomplete derived class

The derived type is incomplete during base class construction, making it unsafe to call derived methods from a base constructor. The object layout for Derived does not exist yet; the vtable pointer (if any) has not been set, and the static_cast reaches memory that has not been initialized.

cpp
// C++11 — DO NOT do this
template<typename D>
struct EagerBase {
    EagerBase() {
        // UNDEFINED BEHAVIOR: D is still being constructed;
        // D's members are uninitialized at this point
        static_cast<D*>(this)->on_init();
    }
};

struct Widget : EagerBase<Widget> {
    int value{42};                         // not yet initialized when on_init() runs
    void on_init() { assert(value == 42);  // FAILS — value is 0 or garbage
    }
};

The correct pattern is to defer initialization to a factory function or a two-phase init() call:

cpp
// C++11 — safe two-phase initialization
template<typename D>
struct LazyBase {
    void init() {
        static_cast<D*>(this)->on_init();  // safe: called after full construction
    }
};

struct Widget : LazyBase<Widget> {
    int value{42};
    void on_init() { assert(value == 42); }  // safe
};

Widget w;
w.init();  // caller is responsible for calling init after construction

Alternatively, use a static factory method that constructs the object and then calls init() in one step, preventing the two-phase anti-pattern from leaking into user code.

See Also

  • std::enable_shared_from_this — standard library CRTP base for safe shared_ptr self-reference
  • Type Erasure — runtime-polymorphic alternative when heterogeneous containers are needed
  • Mixins — non-CRTP behavior injection patterns
  • Concepts — C++20 mechanism for constraining CRTP base requirements
  • Templates — template fundamentals underlying CRTP