Curiously Recurring Template Pattern
CRTP is a template pattern where a derived class inherits from a template base class, and the derived class is the type argument for the base class. So something like this:
template<typename T>
class Base {
Base() = default;
friend class Derived; // allow Derived to construct Base
// This private constructor makes the Base only constructable
// from the Derived class. This prevents us from using Base incorrectly
// static_asserts or SFINAE could also be used
};
class Derived : public Base<Derived> {
};
So what's the purpose of this? Well with this setup, code in the Base class automatically knows what the dynamic type is because the actual type is passed as a template argument.
This makes it safe to static_cast
instead of dynamic_cast
which saves the runtime cost of RTTI type checks. This ability can be used to implement a generic version of
mixins.
In OOP, a mixin is basically a small class that provides a named set of functionality to different types. These mixins are not designed to stand on their own and are typically narrow in scope.
For example, you can have a mixin that might provide serialization functionality. Instead of each class having a to_json
and from_json
method, you can have all classes that you might want
JSON serialization functionality for inherit JSONSerializationMixin
which provides said methods.
As you can imagine, there isn't necessarily much in common between the "children" of a mixin; it doesn't define a hierarchy.
You can have a Device
class use the mixin, and a Student
class; there isn't necessarily a "sibling" relationship.
So back to CRTP, we can use it as a way to add functionality, similar to a mixin, that can be shared between different classes.
template<typename T>
class Geometry {
Geometry() = default;
friend class Square;
friend class Rectangle;
int geom_length() {
if constexpr (std::is_same_v<T, Square>) {
return static_cast<T&>(*this).sideLen();
// static cast is safe
} else {
return static_cast<T&>(*this).length();
}
}
int geom_width() {
if constexpr (std::is_same_v<T, Square>) {
return static_cast<T&>(*this).sideLen();
} else {
return static_cast<T&>(*this).width();
}
}
public:
int area() {
return geom_length() * geom_width();
}
int perimeter() {
return 2 * geom_length() + 2 * geom_width();
}
int volume(int height) {
return area() * height;
}
};
class Square : public Geometry<Square> {
puiblic:
int sideLen() { /*...*/ }
};
class Rectangle : public Geometry<Rectangle> {
puiblic:
int length() { /*...*/ }
int width() { /*...*/ }
};
static_cast
won't remove const
, so if calling a constant member you must cast to a const
reference to the derived type.
//Usage:
Square s;
s.area();
template<typename T>
void doFoo(Geometry<T> & geom) {
// being a type argument to Geometry already limits what T can be
// no need for SFINAE or even a requires clause in the spec
std::cout << geom.volume(10) << std::endl;
}
Rectangle r;
doFoo(r);
doFoo(s);
A benefit of this over template non-member functions is that this makes it very clear which classes support which helper functions.
Consider that we also had a Circle
class. As written, the functions in Geometry
won't support our Circle
, so if Geometry
was a set of non-member functions,
we'd have to document that they didn't support Circle
and we could also use SFINAE to ensure the user doesn't accidentally pass a Circle
.
And look, I'm basically a crazy masochist and even I get tired of writing out all the structs to emulate C++20 concepts when I can't use them.
Furthermore, it's an added burden on the user to have to remember what helper functions use which classes. CRTP also makes these non-member functions clearly part of the interface of Square and Rectangle.
This isn't to say that CRTP is the best solution, but it's an alternative.
I view common applications of CRTP and SFINAE as defensive programming techniques. The goal here is to make your interfaces idiot proof. Imagine there is a little gremlin (maybe you at 2AM) who is trying to use your interfaces incorrectly; template non-member functions that bind to any type are good targets for this nasty little guy. Both SFINAE and CRTP can be applied to restrict what types can be passed to these functions, thus making them more protected from misuse.
Notice that CRTP doesn't use any virtual functions. While this does cut down on runtime cost, it also makes functions in the base class susceptible to shadowing. If you use this pattern, you should ensure that the base class members have different names than any member in any derived class.
So that's one application of CRTP. Another one is to create static interfaces similar to how the STL container adapters work.
The advantage with CRTP over an adapter is that we can use private members. Let's create an interface that provides a size
method.
template<class T>
class Container {
Container() = default;
friend class MyRange;
friend class MyVec;
public:
size_t size() const {
return static_cast<const T&>(*this).size();
}
};
class MyRange : public Container<MyRange> {
public:
size_t size() const {
return std::distance(begin, end);
// imagine this class stores a begin and end iterator
}
};
class MyVec : public Container<MyVec> {
public:
size_t size() const {
return dataSize;
// dataSize could be an internal member storing size
}
};
Notice the subtle difference in flow of control. In this example, the implementation is in the derived class while in the previous one the implementation was in the base class.
The name collision in this case is safe since the base and derived class size
member functions do the same thing.
Now to use this:
MyRange mr;
MyVec mv;
template<typename T>
void doFoo(const Container<T> & container) {
std::cout << container.size() << std::endl;
}
doFoo(mr);
doFoo(mv);