CRTP(Curiously recurring template pattern)

C++
Author

Quasar

Published

December 28, 2024

Introduction

CRTP consists in

  • Inheriting from a template class
  • use the derived class itself as a template parameter of the base class

This is what it looks like in code:

template<typename DerivedType>
struct Base{
    /* ... */
};

struct Derived : Base<Derived>{
    /* ... */
};

The purpose of doing this is to use the derived class in the base class. From the perspective of the base object, the derived object is itself but downcasted. Therefore, the base class can access the derived class by static_casting itself into the derived class.

template<typename DerivedType>
class Base{
    public:
    void doWorkHelper(){
        DerivedType& derived = static_cast<DerivedType&>(*this);
        // use derived
    }
};

class Derived : Base<Derived>{
    /* ... */
};

On Jonathan Boccara’s fluentcpp blog, he talks about how to avoid some slip ups. For example, let’s say you have two derived classes Bisection and NewtonRaphson. Suppose you accidentally end up inheriting from the wrong base class.

template<typename Derived>
struct Solver{
    double solver_helper(auto func, double epsilon){
        Derived& solverImpl = static_cast<Derived&>(*this);
        solverImpl.solve();
    }
}

struct Bisection : Solver<Bisection>{
    double solve(auto func, double epsilon){
        std::cout << "\n" << "Bisection::solve()";
        return 0;
    }
};

struct NewtonRaphson : Solver<Bisection>{
    double solve(auto func, double epsilon){
        std::cout << "\n" << "NewtonRaphson::solve()";
        return 0;
    }
};

To create objects of the derived types Bisection and NewtonRaphson, the derived class constructors have to call the base class constructors. Suppose we make the constructor of the base class private and the Base<DerivedType> class friends with the DerivedType class. Then, Bisection will be friends with Solver<Bisection>, thus Bisection constructor can only invoke the private constructor of Solver<Bisection>. And NewtonRaphson will be friends with Solver<NewtonRaphson>, and it can invoke the private c’tor of only Solver<NewtonRaphson, not Solver<BisectionRaphson>.

#include <iostream>

template<typename DerivedType>
struct Solver{
    private:
    Solver() = default;
    ~Solver() = default;
    friend DerivedType;

    public:
    double solver_helper(auto func, double epsilon){
        DerivedType& solverImpl = static_cast<DerivedType&>(*this);
        solverImpl.solve();
    }
};

struct Bisection : Solver<Bisection>{
    double solve(auto func, double epsilon){
        std::cout << "\n" << "Bisection::solve()";
        return 0;
    }
};

struct NewtonRaphson : Solver<Bisection>{
    double solve(auto func, double epsilon){
        std::cout << "\n" << "NewtonRaphson::solve()";
        return 0;
    }
};

int main(){
    Bisection bisectSolver;
    // NewtonRaphson nrSolver;   compile-error
    return 0;
}

Compiler Explorer

The benefits of CRTP

It’s super-easy to forget the mechanics of CRTP. So, it’s a nice thing to understand what benefits CRTP actually brings to the table.

Adding functionality

Let’s take the example of a class representing vector \(\mathbf{x} \in \mathbf{R}^n\). It

#include <cmath>
#include <iostream>
#include <array>

template<typename ContainerType>
struct Vector{
    ContainerType m_x;

    Vector() = default;

    Vector(ContainerType v)
    : m_x{v}
    {}

    auto operator+(auto& other){
        ContainerType result;
        size_t k{0};
        static_assert(m_x.size() == other.m_x.size());
        for(auto i{m_x.begin()}, j{other.m_x.begin()};i!=m_x.end();++i,++j)
            result[k] = *i + *j;

        return result;
    }

    Vector(const Vector& v)
    : m_x{v.m_x}
    {}

    auto scalarMultiply(double k){
        ContainerType result{m_x};
        for(size_t i{0};i<m_x.size();++i)
            result[i] = k * m_x[i];
        return result;
    }
};

int main(){
    Vector v1{std::array<double,3>{1,2,3}};
    Vector v2{std::array<double,3>{6,7,8}};
    Vector result = v1 + v2;

    Vector v4 = v1.scalarMultiply(2.0);
    return 0;
}

Compiler Explorer

Now, imagine that we have another class, for example Complex that also needs element-wise addition and scalar multiplication. This is where CRTP comes into play. We can factor out the operator+() and scalarMultiply() functions into a separate class:

#include <cmath>
#include <iostream>
#include <array>

template<typename DerivedType, typename ContainerType>
struct ComponentWiseOperations{
    auto operator+(auto& other){
        DerivedType& derived = static_cast<DerivedType&>(*this);
        ContainerType result;
        size_t k{0};

        for(auto i{derived.m_x.begin()}, j{other.m_x.begin()};i!=derived.m_x.end();++i,++j)
            result[k] = *i + *j;

        return result;
    }

    auto scalarMultiply(double k){
        DerivedType& derived = static_cast<DerivedType&>(*this);
        ContainerType result{derived.m_x};
        for(size_t i{0};i<derived.m_x.size();++i)
            result[i] = k * derived.m_x[i];
        return result;
    }
};

and use the CRTP to allow Vector to use it.

template<typename ContainerType>
struct Vector : public ComponentWiseOperations<Vector<ContainerType>,ContainerType>{
    ContainerType m_x;

    Vector() = default;

    Vector(ContainerType v)
    : m_x{v}
    {}

    Vector(const Vector& v)
    : m_x{v.m_x}
    {}

    auto operator[](int n){
        return m_x[n];
    }
};

Compiler Explorer

The interface in CRTP

As Jonathan Boccara writes, although CRTP uses ineritance, its usage of it does not have the same meaning as other cases of inheritance.

In general, when a derived class inherits from the base class, it expresses the idea that the derived class conceptually is a base class. The purpose is to use the base class in generic code. Calls to base class code are redirected over to code in the derived class.

With the CRTP, the derived class does not express the fact that it is a base class. Rather, it expands its interface by inheriting from the base class, in order to add more functionality. So, you make calls directly to the derived class and never use the base class directly.

The base class is not the interface and the derived clas is not the implementation. Rather it is the other way round: the base class uses the derived class methods.

Limiting the object count with CRTP

One scenario in which the CRTP idiom could be applied is to limit the object count. We code up a limit_instances class template and have our user-declared classes specialize from it. The limit_instances class has a class(static) member variable called count that keeps track of the number instances of the user-objects. Each time the user-object constructor(and thus limit_instances base class constructor) is invoked, we are to increment count and every destructor call decrements the count.

#include <iostream>
#include <exception>
#include <atomic>

template<typename T, int N>
struct limit_instances{
    static int count;

    limit_instances(){
        if(count >= N)
            throw std::logic_error("Too many instances.");
        ++count;
    }

    ~limit_instances(){
        --count;
    }
};

template<typename T, int N>
int limit_instances<T,N>::count = 0;

struct X : public limit_instances<X,3>{
    /*
        User-defined private member fields
        and public methods.
    */
};

struct Y : public limit_instances<Y,5>{
    /*
        ...
    */
};

int main()
{
    X x1,x2,x3;   // okay
    try{
        X x4;         
    }
    catch(std::exception& e){
        std::cout << e.what() << "\n";
    }
    
    Y y[5];       // okay
     try{
        Y anotherInstance;         
    }
    catch(std::exception& e){
        std::cout << e.what() << "\n";
    } 
    return 0;
}

Compiler Explorer

The class template limit_instances takes the user-class T and the maximum number of allowable instances N as template parameters. So, the user-class when inheriting from limit_instances and while invoking the base-class constructor should supply appropriate template arguments.

Implementing the Composite design pattern

The composite design pattern lets you compose objects into tree structures.

CRTP can be used to implement the composite design pattern. We can quickly code it up:

#include <vector>
#include <iostream>
#include <string>

template<typename T>
struct Component{
    template<typename U>
    void connect(U& other);

    std::string name;
    Component(){}
    Component(std::string name_) : name(name_) {}

};

struct Node : Component<Node>{
    Node* begin(){
        return this;
    }    

    Node* end(){
        return (this + 1);
    }

    Node(std::string name_) : Component<Node>(name_) {}
    std::vector<Node*> connections;
};

struct Composite : std::vector<Node>, Component<Composite>{
    Composite() : Component<Composite>(){}
    Composite(std::string name_) : Component<Composite>(name_){}
};

template<typename T>
template<typename U>
void Component<T>::connect(U& other){
    for(Node& from : *static_cast<T*>(this)){
        for(Node& to : other){
            from.connections.push_back(&to);
            to.connections.push_back(&from);
        }
    }
}

int main(){
    
    Node a("Gregory Peck");
    
    Node b("Marlon Brando");
    Node c("Audrey Hepburn");
    Node d("Charles Chaplin");

    Node guitarist("Jimmy Hendrix");
    Node artistParExcellence("Elvis Presley");
    
    Composite actorsTroupe;
    actorsTroupe.push_back(a);
    actorsTroupe.push_back(b);
    actorsTroupe.push_back(c);
    actorsTroupe.push_back(d);

    guitarist.connect(actorsTroupe);
    actorsTroupe.connect(artistParExcellence);
    
    return 0;
}

This helps avoid the explosion of state-space and writing methods such as Node::connect(Node&), Node::connect(Composite&), Composite::connect(Node&) and Node::connect(Node&).