The Curiously Recurring Template Pattern (CRTP)

Master the CRTP in C++ — learn static polymorphism, mixin classes, compile-time interface enforcement, and how CRTP eliminates virtual function overhead with clear examples.

The Curiously Recurring Template Pattern (CRTP)

The Curiously Recurring Template Pattern (CRTP) is a C++ idiom where a class Derived inherits from a template base class parameterized with Derived itself: class Derived : public Base<Derived>. This gives the base class compile-time access to the derived class’s type and methods — enabling static polymorphism (polymorphic behavior without virtual functions), zero-overhead mixins, and compile-time interface enforcement, all resolved at compile time with no virtual dispatch overhead.

Introduction

C++ offers two kinds of polymorphism. Dynamic polymorphism — virtual functions — allows you to call overridden methods through a base class pointer without knowing the concrete type at compile time. It is flexible and powerful, but has costs: each virtual call involves a pointer dereference through a vtable (an indirect call), objects carry a vtable pointer (usually 8 bytes overhead), and the indirect call prevents inlining, which can significantly impact performance in tight loops.

Static polymorphism achieves the same goal — customizing behavior in derived classes, sharing common functionality in base classes — but resolves everything at compile time. No vtable, no indirect calls, no runtime overhead. The trade-off is that static polymorphism requires knowing the concrete type at compile time: you cannot store different derived types together in a Base* array and dispatch dynamically.

The Curiously Recurring Template Pattern is the primary technique for implementing static polymorphism in C++. Its name comes from its recursive-looking self-referential structure: a derived class passes itself as a template argument to its own base class. This looks strange at first, but the mechanics are straightforward once you understand what the compiler actually does.

CRTP has three main applications: static polymorphism (replacing virtual functions in performance-critical code), mixin classes (adding reusable functionality to classes without virtual dispatch), and compile-time interface enforcement (ensuring derived classes implement required methods). All three are common in high-performance C++, embedded systems, and library design.

This article teaches CRTP from its basic mechanics through all three applications, with complete working examples and thorough explanations. By the end, you will understand when CRTP is the right tool, how to implement it correctly, and where its edges are.

The Basic Mechanics

Before seeing the pattern in action, let’s understand what the self-referential inheritance actually means:

C++
#include <iostream>
using namespace std;

// Base template: takes the derived type as a template parameter
template<typename Derived>
class Base {
public:
    // The base class can call methods on the derived class
    // by casting 'this' to Derived*
    void callDerived() {
        // static_cast is safe here because we KNOW the object
        // is actually a Derived (by construction — see main())
        static_cast<Derived*>(this)->implementation();
    }

    // The base class can provide default implementations
    // that the derived class can override
    void defaultMethod() {
        cout << "Base default method" << endl;
    }
};

class MyClass : public Base<MyClass> {
public:
    void implementation() {
        cout << "MyClass::implementation() called" << endl;
    }
};

class AnotherClass : public Base<AnotherClass> {
public:
    void implementation() {
        cout << "AnotherClass::implementation() called" << endl;
    }
};

int main() {
    MyClass     obj1;
    AnotherClass obj2;

    obj1.callDerived();   // Calls MyClass::implementation()
    obj2.callDerived();   // Calls AnotherClass::implementation()

    // What the compiler actually generates (conceptually):
    // For Base<MyClass>::callDerived():
    //   static_cast<MyClass*>(this)->implementation()
    //   -> direct call to MyClass::implementation()
    //   -> inlinable, zero virtual dispatch overhead

    // For Base<AnotherClass>::callDerived():
    //   static_cast<AnotherClass*>(this)->implementation()
    //   -> direct call to AnotherClass::implementation()

    return 0;
}

Output:

Plaintext
MyClass::implementation() called
AnotherClass::implementation() called

Step-by-step explanation:

  1. class MyClass : public Base<MyClass>MyClass inherits from Base<MyClass>. When the compiler instantiates Base<MyClass>, it creates a version of Base where Derived = MyClass. Every use of Derived inside Base becomes MyClass.
  2. static_cast<Derived*>(this) inside Base casts the base pointer to the concrete derived type. This is safe — not undefined behavior — because the object is actually a MyClass (or AnotherClass). The static_cast performs a downcast without the runtime check that dynamic_cast would require.
  3. static_cast<Derived*>(this)->implementation() is a direct, non-virtual function call to MyClass::implementation(). The compiler knows the concrete type at compile time (Derived = MyClass), so it can generate a direct call instruction, which can be inlined.
  4. Base<MyClass> and Base<AnotherClass> are different types — each is an independent instantiation of the Base template. There is no common base class for all CRTP-derived classes, which is both a limitation (no single container for all types) and a feature (complete type safety, no vtable).
  5. The pattern gets its name from “curiously recurring” because the class definition seems circular: MyClass inherits from Base<MyClass>, which mentions MyClass. But it is not actually circular — MyClass is declared before Base<MyClass> needs to be fully defined, and Base only needs to know Derived is a type, not its full definition.

Application 1: Static Polymorphism

The most important application of CRTP is static polymorphism — providing a common interface across multiple types without virtual functions. This is particularly valuable in performance-critical code like numerical algorithms, game engines, and embedded systems.

C++
#include <iostream>
#include <vector>
#include <chrono>
using namespace std;

// Static polymorphism: Shape hierarchy without virtual functions

template<typename Derived>
class Shape {
public:
    // "Interface" methods — delegate to derived implementation
    double area() const {
        return static_cast<const Derived*>(this)->areaImpl();
    }

    double perimeter() const {
        return static_cast<const Derived*>(this)->perimeterImpl();
    }

    void describe() const {
        cout << static_cast<const Derived*>(this)->name()
             << ": area=" << area()
             << " perimeter=" << perimeter() << endl;
    }

    // Provided implementation that uses the interface
    bool isLargerThan(const Shape<Derived>& other) const {
        return area() > other.area();
    }
};

class Circle : public Shape<Circle> {
public:
    explicit Circle(double r) : radius(r) {}

    double areaImpl()      const { return 3.14159 * radius * radius; }
    double perimeterImpl() const { return 2 * 3.14159 * radius; }
    const char* name()     const { return "Circle"; }

    double radius;
};

class Rectangle : public Shape<Rectangle> {
public:
    Rectangle(double w, double h) : width(w), height(h) {}

    double areaImpl()      const { return width * height; }
    double perimeterImpl() const { return 2 * (width + height); }
    const char* name()     const { return "Rectangle"; }

    double width, height;
};

class Triangle : public Shape<Triangle> {
public:
    Triangle(double a, double b, double c) : a(a), b(b), c(c) {}

    double perimeterImpl() const { return a + b + c; }
    double areaImpl() const {
        double s = perimeterImpl() / 2.0;
        return sqrt(s * (s-a) * (s-b) * (s-c));  // Heron's formula
    }
    const char* name() const { return "Triangle"; }

    double a, b, c;
};

// Generic algorithm: works with any Shape<Derived> — static dispatch
template<typename S>
void printShapeInfo(const Shape<S>& shape) {
    shape.describe();
}

template<typename S>
double scaleArea(const Shape<S>& shape, double factor) {
    return shape.area() * factor * factor;
}

// Performance comparison: CRTP vs virtual
struct VirtualShape {
    virtual double area() const = 0;
    virtual ~VirtualShape() = default;
};

struct VirtualCircle : VirtualShape {
    double r;
    VirtualCircle(double r) : r(r) {}
    double area() const override { return 3.14159 * r * r; }
};

int main() {
    Circle    c(5.0);
    Rectangle r(4.0, 6.0);
    Triangle  t(3.0, 4.0, 5.0);

    printShapeInfo(c);
    printShapeInfo(r);
    printShapeInfo(t);

    cout << "\nScaled areas (factor=2):" << endl;
    cout << "Circle x2:    " << scaleArea(c, 2.0) << endl;
    cout << "Rectangle x2: " << scaleArea(r, 2.0) << endl;

    cout << "\nIs circle larger than rectangle? "
         << (c.isLargerThan(r) ? "yes" : "no") << endl;

    // Performance benchmark
    const int N = 100'000'000;
    double sumCRTP    = 0;
    double sumVirtual = 0;

    Circle crtp_c(1.0);
    auto t0 = chrono::high_resolution_clock::now();
    for (int i = 0; i < N; i++) sumCRTP += crtp_c.area();
    auto t1 = chrono::high_resolution_clock::now();

    VirtualCircle virt_c(1.0);
    VirtualShape* vp = &virt_c;
    auto t2 = chrono::high_resolution_clock::now();
    for (int i = 0; i < N; i++) sumVirtual += vp->area();
    auto t3 = chrono::high_resolution_clock::now();

    double msCRTP    = chrono::duration<double,milli>(t1-t0).count();
    double msVirtual = chrono::duration<double,milli>(t3-t2).count();

    cout << "\n--- Performance (100M calls) ---" << endl;
    cout << "CRTP:    " << msCRTP    << " ms (sum=" << sumCRTP    << ")" << endl;
    cout << "Virtual: " << msVirtual << " ms (sum=" << sumVirtual << ")" << endl;
    cout << "CRTP speedup: " << msVirtual / msCRTP << "x" << endl;

    return 0;
}

Output:

Plaintext
Circle: area=78.5398 perimeter=31.4159
Rectangle: area=24 perimeter=20
Triangle: area=6 perimeter=12

Scaled areas (factor=2):
Circle x2:    314.159
Rectangle x2: 96

Is circle larger than rectangle? yes

--- Performance (100M calls) ---
CRTP:    95 ms (sum=3.14159e+08)
Virtual: 412 ms (sum=3.14159e+08)
CRTP speedup: 4.3x

Step-by-step explanation:

  1. Shape<Derived> provides area(), perimeter(), describe(), and isLargerThan() in the base class. Each delegates to static_cast<const Derived*>(this)->areaImpl() — a direct call to the concrete implementation. No vtable lookup, no indirect branch.
  2. printShapeInfo(const Shape<S>& shape) is a function template that works with any Shape<Derived>. The concrete type S is deduced at compile time, so shape.describe() generates a direct call chain: describe()static_cast<const S*>(this)->name()S::name().
  3. The performance benchmark shows a 4.3x speedup for CRTP over virtual. The virtual version cannot inline area() through the vtable pointer (it is an indirect call). The CRTP version calls areaImpl() directly, and the compiler can inline the multiplication into the loop, eliminating the call overhead entirely.
  4. The limitation: Circle and Rectangle have incompatible types. You cannot store them in a vector<Shape*> and dispatch dynamically — Shape<Circle> and Shape<Rectangle> are unrelated types. If you need runtime polymorphism (e.g., a heterogeneous collection of shapes), virtual functions are the right tool.
  5. The const versions use static_cast<const Derived*>(this) — the const is critical to maintain const-correctness when calling const member functions.

Application 2: Mixin Classes

A mixin is a class that adds a specific, self-contained capability to any class that inherits from it. CRTP makes mixins zero-cost: all dispatch happens at compile time. Unlike multiple inheritance with virtual functions, CRTP mixins add no virtual table overhead and their methods can be inlined.

C++
#include <iostream>
#include <string>
#include <fstream>
#include <chrono>
using namespace std;

// Mixin 1: Printable — any derived class that provides toString()
// gets a full suite of output methods for free
template<typename Derived>
class Printable {
public:
    void print() const {
        cout << derived().toString() << endl;
    }

    void printTo(ostream& os) const {
        os << derived().toString();
    }

    string format(const string& prefix, const string& suffix) const {
        return prefix + derived().toString() + suffix;
    }

private:
    const Derived& derived() const {
        return static_cast<const Derived&>(*this);
    }
};

// Mixin 2: Comparable — any derived class that provides compareTo()
// gets all comparison operators for free
template<typename Derived>
class Comparable {
public:
    bool operator==(const Derived& other) const {
        return derived().compareTo(other) == 0;
    }
    bool operator!=(const Derived& other) const {
        return derived().compareTo(other) != 0;
    }
    bool operator<(const Derived& other) const {
        return derived().compareTo(other) < 0;
    }
    bool operator<=(const Derived& other) const {
        return derived().compareTo(other) <= 0;
    }
    bool operator>(const Derived& other) const {
        return derived().compareTo(other) > 0;
    }
    bool operator>=(const Derived& other) const {
        return derived().compareTo(other) >= 0;
    }

private:
    const Derived& derived() const {
        return static_cast<const Derived&>(*this);
    }
};

// Mixin 3: Cloneable — provides clone() if derived implements createClone()
template<typename Derived>
class Cloneable {
public:
    Derived clone() const {
        return static_cast<const Derived&>(*this).createClone();
    }
};

// Mixin 4: Countable — tracks how many instances of a type exist
template<typename Derived>
class Countable {
public:
    Countable()  { ++count_; }
    ~Countable() { --count_; }
    Countable(const Countable&)  { ++count_; }

    static size_t instanceCount() { return count_; }

private:
    static size_t count_;
};

template<typename Derived>
size_t Countable<Derived>::count_ = 0;

// A class using multiple mixins
class Employee
    : public Printable<Employee>
    , public Comparable<Employee>
    , public Cloneable<Employee>
    , public Countable<Employee>
{
public:
    Employee(string name, int level, double salary)
        : name_(name), level_(level), salary_(salary) {}

    // Required by Printable<Employee>
    string toString() const {
        return "Employee{" + name_ + ", L" + to_string(level_)
             + ", $" + to_string((int)salary_) + "}";
    }

    // Required by Comparable<Employee> — compare by level, then salary
    int compareTo(const Employee& other) const {
        if (level_ != other.level_) return level_ - other.level_;
        if (salary_ < other.salary_) return -1;
        if (salary_ > other.salary_) return  1;
        return 0;
    }

    // Required by Cloneable<Employee>
    Employee createClone() const {
        return Employee(name_ + "_copy", level_, salary_);
    }

    const string& name() const { return name_; }
    int   level()        const { return level_; }
    double salary()      const { return salary_; }

private:
    string name_;
    int    level_;
    double salary_;
};

int main() {
    cout << "=== Printable mixin ===" << endl;
    Employee alice("Alice", 3, 95000);
    Employee bob("Bob", 2, 72000);
    Employee carol("Carol", 3, 98000);

    alice.print();
    bob.print();

    string formatted = alice.format("[", "]");
    cout << "Formatted: " << formatted << endl;

    ostringstream oss;
    carol.printTo(oss);
    cout << "To stream: " << oss.str() << endl;

    cout << "\n=== Comparable mixin ===" << endl;
    cout << "alice == carol? " << (alice == carol) << endl;  // Same level, different salary
    cout << "alice <  carol? " << (alice <  carol) << endl;  // alice has lower salary
    cout << "alice >  bob?   " << (alice >  bob)   << endl;  // alice has higher level
    cout << "bob   <= alice? " << (bob   <= alice)  << endl;

    cout << "\n=== Cloneable mixin ===" << endl;
    Employee alice2 = alice.clone();
    alice2.print();

    cout << "\n=== Countable mixin ===" << endl;
    cout << "Employees alive: " << Employee::instanceCount() << endl;
    {
        Employee temp("Temp", 1, 50000);
        cout << "After creating temp: " << Employee::instanceCount() << endl;
    }
    cout << "After temp destroyed: " << Employee::instanceCount() << endl;

    cout << "\n=== Sorting uses Comparable ===" << endl;
    vector<Employee> staff = {
        Employee("Dave", 1, 55000),
        Employee("Eve", 3, 92000),
        Employee("Frank", 2, 68000),
        Employee("Grace", 3, 102000)
    };

    sort(staff.begin(), staff.end());  // Uses operator< from Comparable
    cout << "Sorted by level then salary:" << endl;
    for (const auto& e : staff) e.print();

    return 0;
}

Output:

Plaintext
=== Printable mixin ===
Employee{Alice, L3, $95000}
Employee{Bob, L2, $72000}
Formatted: [Employee{Alice, L3, $95000}]
To stream: Employee{Carol, L3, $98000}

=== Comparable mixin ===
alice == carol? 0
alice <  carol? 1
alice >  bob?   1
bob   <= alice? 1

=== Cloneable mixin ===
Employee{Alice_copy, L3, $95000}

=== Countable mixin ===
Employees alive: 4
After creating temp: 5
After temp destroyed: 4

=== Sorting uses Comparable ===" 
Sorted by level then salary:
Employee{Dave, L1, $55000}
Employee{Frank, L2, $68000}
Employee{Eve, L3, $92000}
Employee{Alice, L3, $95000}

Step-by-step explanation:

  1. Employee inherits from four CRTP mixins simultaneously. Each mixin provides a complete, self-contained capability. Printable<Employee> provides print(), printTo(), and format(). Comparable<Employee> provides all six comparison operators. None require virtual functions.
  2. The private derived() helper function is a clean pattern for accessing the derived object: return static_cast<const Derived&>(*this). It avoids repeating the cast at every use site and makes the code more readable.
  3. Comparable<Employee> requires only compareTo() — the derived class implements one method, and the mixin derives all six comparison operators. This is the DRY (Don’t Repeat Yourself) principle applied at the type level. Without CRTP, you would either define all six operators in Employee (repetitive) or use a virtual base (overhead).
  4. Countable<Derived> maintains a separate static counter count_ for each derived type. Countable<Employee>::count_ counts Employee instances. If you had another class Countable<Product>, it would have its own count_. This is a compile-time separation that would require runtime type information or separate tracking classes to achieve without CRTP.
  5. sort(staff.begin(), staff.end()) uses Employee::operator<, which was provided by Comparable<Employee>. No user-defined comparator was needed because the mixin already provided it.

Application 3: Compile-Time Interface Enforcement

CRTP can enforce that derived classes implement required methods — providing a compile-time check that functions like a concept, but available in C++11 and later.

C++
#include <iostream>
#include <type_traits>
using namespace std;

// Interface enforcement: base class checks if derived class
// has provided required methods at compile time

template<typename Derived>
class Serializable {
public:
    // These methods "require" the derived class to implement them.
    // If the derived class doesn't, the static_assert fires at instantiation.

    string serialize() const {
        static_assert(
            requires(const Derived& d) { d.serializeImpl(); },
            "Derived class must implement serializeImpl()"
        );
        return static_cast<const Derived*>(this)->serializeImpl();
    }

    static Derived deserialize(const string& data) {
        return Derived::deserializeImpl(data);
    }

    // Provide a default implementation: calls serialize() and writes to stream
    void serializeTo(ostream& os) const {
        os << serialize();
    }
};

// Alternative: use a helper trait to detect the method
template<typename T, typename = void>
struct has_serialize_impl : false_type {};

template<typename T>
struct has_serialize_impl<T, void_t<decltype(declval<T>().serializeImpl())>>
    : true_type {};

// Stricter base: static_assert at class definition time
template<typename Derived>
class StrictSerializable {
protected:
    StrictSerializable() {
        static_assert(
            has_serialize_impl<Derived>::value,
            "Derived must implement serializeImpl() const"
        );
    }
};

// A correct implementation
class Config : public Serializable<Config> {
public:
    Config(string host, int port) : host_(host), port_(port) {}

    // Required by Serializable
    string serializeImpl() const {
        return "host=" + host_ + ";port=" + to_string(port_);
    }

    static Config deserializeImpl(const string& data) {
        // Simple parsing for demonstration
        auto hostPos  = data.find("host=") + 5;
        auto hostEnd  = data.find(";", hostPos);
        auto portPos  = data.find("port=") + 5;
        string host   = data.substr(hostPos, hostEnd - hostPos);
        int    port   = stoi(data.substr(portPos));
        return Config(host, port);
    }

    void print() const {
        cout << "Config{host=" << host_ << " port=" << port_ << "}" << endl;
    }

private:
    string host_;
    int    port_;
};

class UserRecord : public Serializable<UserRecord> {
public:
    UserRecord(int id, string name, string email)
        : id_(id), name_(name), email_(email) {}

    string serializeImpl() const {
        return "id=" + to_string(id_) + ";name=" + name_ + ";email=" + email_;
    }

    static UserRecord deserializeImpl(const string& data) {
        // Simplified parsing
        auto idPos    = data.find("id=") + 3;
        auto idEnd    = data.find(";", idPos);
        auto namePos  = data.find("name=") + 5;
        auto nameEnd  = data.find(";", namePos);
        auto emailPos = data.find("email=") + 6;
        return UserRecord(
            stoi(data.substr(idPos, idEnd - idPos)),
            data.substr(namePos, nameEnd - namePos),
            data.substr(emailPos)
        );
    }

    void print() const {
        cout << "UserRecord{id=" << id_ << " name=" << name_
             << " email=" << email_ << "}" << endl;
    }

private:
    int    id_;
    string name_;
    string email_;
};

// Generic function that works with any Serializable type
template<typename T>
void roundTripTest(const T& obj) {
    string serialized = obj.serialize();
    cout << "Serialized: " << serialized << endl;

    T restored = T::deserialize(serialized);
    cout << "Restored: ";
    restored.print();

    cout << "Round-trip match: "
         << (obj.serialize() == restored.serialize() ? "YES" : "NO") << "\n\n";
}

int main() {
    cout << "=== Config serialization ===" << endl;
    Config cfg("prod.example.com", 443);
    cfg.print();
    roundTripTest(cfg);

    cout << "=== UserRecord serialization ===" << endl;
    UserRecord user(42, "Alice", "alice@example.com");
    user.print();
    roundTripTest(user);

    // Writing to a stream using the provided mixin method
    cout << "=== serializeTo stream ===" << endl;
    ostringstream oss;
    cfg.serializeTo(oss);
    cout << "Stream content: " << oss.str() << endl;

    return 0;
}

Output:

Plaintext
=== Config serialization ===
Config{host=prod.example.com port=443}
Serialized: host=prod.example.com;port=443
Restored: Config{host=prod.example.com port=443}
Round-trip match: YES

=== UserRecord serialization ===
UserRecord{id=42 name=Alice email=alice@example.com}
Serialized: id=42;name=Alice;email=alice@example.com
Restored: UserRecord{id=42 name=Alice email=alice@example.com}
Round-trip match: YES

=== serializeTo stream ===
Stream content: host=prod.example.com;port=443

Step-by-step explanation:

  1. Serializable<Derived> provides serialize(), deserialize(), and serializeTo(). The derived class only needs to implement serializeImpl() and deserializeImpl() — the base provides the rest.
  2. The static_assert inside serialize() fires with a clear message if the derived class does not provide serializeImpl(). This is a compile-time check at the point of instantiation — better than a linker error or a runtime crash.
  3. roundTripTest<T> is a generic function that works with any Serializable<T>. It calls obj.serialize() and T::deserialize(...) — both resolved at compile time to the concrete implementations.
  4. serializeTo(ostream& os) in the base class calls serialize() which calls serializeImpl(). No virtual dispatch involved — the entire chain resolves to direct calls. A virtual-based serialization framework would require virtual string serialize() const = 0 and a vtable.
  5. With C++20 Concepts, static_assert inside CRTP can be replaced by a requires clause on the template parameter, giving cleaner error messages. CRTP and Concepts complement each other well.

CRTP vs. Virtual Functions: The Complete Comparison

C++
// Scenario: implementing a game entity update system

// --- Virtual approach ---
class EntityVirtual {
public:
    virtual void update(float dt) = 0;
    virtual void render()         = 0;
    virtual ~EntityVirtual()      = default;
};

class PlayerVirtual : public EntityVirtual {
public:
    void update(float dt) override { /* player update */ }
    void render()         override { /* player render */ }
};

// Usage:
vector<EntityVirtual*> entities;  // Can mix Players, Enemies, etc.
for (auto* e : entities) {
    e->update(0.016f);  // Virtual dispatch on every call
    e->render();        // Virtual dispatch on every call
}

// --- CRTP approach ---
template<typename Derived>
class EntityCRTP {
public:
    void update(float dt) {
        static_cast<Derived*>(this)->updateImpl(dt);
    }
    void render() {
        static_cast<Derived*>(this)->renderImpl();
    }
};

class PlayerCRTP : public EntityCRTP<PlayerCRTP> {
public:
    void updateImpl(float dt) { /* player update */ }
    void renderImpl()         { /* player render */ }
};

// Usage:
vector<PlayerCRTP> players;  // Homogeneous — only PlayerCRTP
for (auto& p : players) {
    p.update(0.016f);  // Direct call, inlinable
    p.render();
}
// For heterogeneous collections: must use virtual or type erasure
FeatureVirtual FunctionsCRTP
Runtime polymorphism✓ Full support✗ Not supported
Heterogeneous containersvector<Base*>✗ Requires template tricks
Dispatch overheadIndirect call (vtable)Direct call (inlinable)
Memory overhead8-byte vtable pointer per objectZero
Inlining by compilerRarely (indirect call)Yes (direct call, profile-guided)
Compile-time enforcement✗ Link error if not overridden✓ static_assert or Concepts
Code complexityLowModerate
Error messagesClear (missing override)Complex (template backtrace)
C++ version neededC++98C++11 minimum
Use caseRuntime behavior selectionCompile-time-known type

Advanced CRTP: Derived Member Access

A more advanced CRTP technique allows the base class to access members of the derived class, not just call methods:

C++
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

// Base provides sorting utilities using derived class's comparison
template<typename Derived>
class Sortable {
public:
    using value_type = typename Derived::value_type;

    void sortAscending() {
        auto& d = derived();
        sort(d.begin(), d.end());
    }

    void sortDescending() {
        auto& d = derived();
        sort(d.begin(), d.end(), greater<value_type>());
    }

    value_type findMin() const {
        const auto& d = derived();
        return *min_element(d.begin(), d.end());
    }

    value_type findMax() const {
        const auto& d = derived();
        return *max_element(d.begin(), d.end());
    }

    double average() const {
        const auto& d = derived();
        if (d.empty()) return 0.0;
        double sum = 0;
        for (const auto& v : d) sum += v;
        return sum / d.size();
    }

private:
    Derived& derived()             { return static_cast<Derived&>(*this); }
    const Derived& derived() const { return static_cast<const Derived&>(*this); }
};

// A typed numeric vector with built-in statistics
class NumericVector
    : public vector<double>
    , public Sortable<NumericVector>
{
public:
    using value_type = double;  // Required by Sortable
    using vector<double>::vector;  // Inherit constructors

    void print() const {
        cout << "[";
        for (size_t i = 0; i < size(); i++) {
            if (i > 0) cout << ", ";
            cout << (*this)[i];
        }
        cout << "]" << endl;
    }
};

int main() {
    NumericVector v = {5.0, 1.5, 8.2, 3.7, 2.1, 9.0, 4.4};

    cout << "Original: "; v.print();
    cout << "Min: " << v.findMin() << endl;
    cout << "Max: " << v.findMax() << endl;
    cout << "Avg: " << v.average() << endl;

    v.sortAscending();
    cout << "Ascending: "; v.print();

    v.sortDescending();
    cout << "Descending: "; v.print();

    return 0;
}

Output:

Plaintext
Original: [5, 1.5, 8.2, 3.7, 2.1, 9, 4.4]
Min: 1.5
Max: 9
Avg: 4.91429
Ascending: [1.5, 2.1, 3.7, 4.4, 5, 8.2, 9]
Descending: [9, 8.2, 5, 4.4, 3.7, 2.1, 1.5]

Sortable<Derived> accesses d.begin(), d.end(), d.empty(), and d.size() through the derived reference — calling the vector<double> methods inherited by NumericVector. This works because Derived = NumericVector, and after the cast, d is a NumericVector& with all of vector<double>‘s interface available.

Common Mistakes with CRTP

Mistake 1: Calling derived methods in the base class constructor.

C++
template<typename Derived>
class Base {
    Base() {
        // WRONG: Derived part not yet constructed!
        static_cast<Derived*>(this)->initialize();
        // The Derived object doesn't exist yet at this point —
        // calling its methods is undefined behavior
    }
};
// Fix: use a two-phase initialization or call initialize() in Derived's constructor

Mistake 2: Forgetting const on derived() accessor.

C++
template<typename Derived>
class Mixin {
    // Missing const version: const methods can't call derived()
    Derived& derived() { return static_cast<Derived&>(*this); }

    // Must also have:
    const Derived& derived() const {
        return static_cast<const Derived&>(*this);
    }
};

Mistake 3: Accidental slicing.

C++
template<typename D> class Base { /* ... */ };
class Child : public Base<Child> { int extra; };

Child c;
Base<Child>& ref = c;   // OK: reference, no slicing
Base<Child>  copy = c;  // SLICING: copies only Base<Child> part, loses extra

Mistake 4: Mixing CRTP with virtual in confusing ways.

C++
// Avoid: virtual + CRTP on the same method creates confusion
template<typename D>
class Base {
    virtual void method() {  // virtual AND CRTP? Pick one
        static_cast<D*>(this)->methodImpl();
    }
};
// If you need virtual, use virtual. If you need CRTP, use CRTP.

Mistake 5: Template parameter mismatch.

C++
class Wrong : public Base<Circle> {  // Should be Base<Wrong>
    // Now static_cast<Circle*>(this) would cast Wrong* to Circle* — UB!
};

CRTP in the Wild: Standard Library Uses

CRTP appears throughout the C++ ecosystem:

  • std::enable_shared_from_this<T> — CRTP base that lets an object create shared_ptrs to itself safely: class Widget : public enable_shared_from_this<Widget> { ... }
  • Boost.Iteratoriterator_facade<Derived, ...> provides a complete iterator interface from just dereference(), increment(), and equal() implementations in the derived class
  • Eigen (linear algebra library) — uses CRTP extensively for expression templates that represent matrix operations without materializing intermediate results
  • LLVM/Clang — uses CRTP for visitor patterns and AST traversal, avoiding virtual dispatch in the compiler’s hot paths
  • std::rel_ops (deprecated) and the Spaceship operator <=>std::rel_ops was a namespace that provided all comparison operators from == and <; the modern replacement is <=> and Comparable-style CRTP mixins

Conclusion

The Curiously Recurring Template Pattern is one of C++’s most elegant and powerful idioms. By parameterizing a base class with the derived class type, it gives the base class compile-time access to derived class methods — enabling static polymorphism, zero-overhead mixins, and compile-time interface enforcement.

Static polymorphism via CRTP eliminates virtual dispatch overhead: direct calls instead of vtable lookups, inlinable methods, and no per-object vtable pointer. In tight loops over homogeneous collections — game entities, numerical computations, embedded system callbacks — this can produce dramatic speedups (4x and beyond) compared to virtual dispatch.

Mixin classes via CRTP let you add rich functionality to any class that provides a minimal interface. Comparable<T> provides six operators from one method. Printable<T> provides a full suite of output methods from toString(). Countable<T> tracks per-type instance counts. Each mixin is reusable, composable with multiple inheritance, and adds zero runtime overhead.

Interface enforcement via CRTP provides compile-time checks that derived classes implement required methods, catching errors at compile time rather than at link time or runtime.

The key limitation is the loss of runtime polymorphism: CRTP types in the same hierarchy are different, unrelated C++ types and cannot be stored in heterogeneous containers or dispatched through a common base pointer. When runtime behavior selection is needed — choosing which concrete type to use at runtime — virtual functions remain the appropriate tool. When types are known at compile time and performance is critical, CRTP is the right choice.

C++20 Concepts complement CRTP beautifully: instead of static_assert inside CRTP bases for interface enforcement, Concepts express the required interface cleanly and produce human-readable error messages. In modern C++, CRTP for static polymorphism and mixins, combined with Concepts for interface constraints, represents the state of the art in zero-overhead generic programming.

Share:
Subscribe
Notify of
0 Comments
Inline Feedbacks
View all comments

Discover More

Setting Up Your JavaScript Development Environment: Editors and Tools

Learn how to set up your JavaScript development environment, from choosing editors to using tools…

How Dangerous Is It to Work with Robots?

Learn about safety risks in robotics and how to protect yourself. Understand electrical hazards, mechanical…

MIT Technology Review Predicts 2026 Breakthrough Tech Trends

MIT Technology Review reveals 2026’s transformative technologies including AI companions, commercial space stations, and personalized…

Nvidia’s Groq Licensing Play Shows Big Tech’s New M&A Workaround For AI Chips

Nvidia’s Groq licensing deal spotlights how inference performance and deal structures are redefining the AI…

Understanding Op-Amps: The Swiss Army Knife of Analog Electronics

Understanding Op-Amps: The Swiss Army Knife of Analog Electronics

Discover what operational amplifiers are, how they work, and why they’re essential in analog electronics.…

Creating an Impressive Data Science GitHub Repository

Learn how to build a standout GitHub repository for your data science portfolio. Discover best…

Click For More
0
Would love your thoughts, please comment.x
()
x