CRTP - Curiously Recurring Template Pattern
Author: thothonegan
Tags: crtp development c++
Lets talk about an interesting thing in C++ : CRTP.
CRTP is the Curiously Recurring Template Pattern. At its simplest, its having a template use the current class as a parameter. e.g.
template
class Parent
{};
class Child : public Parent
{};
Why is this useful? One major case is 'static polymorphism'. First, we have normal polymorphism which works via virtual in C++.
class Object
{
public:
float volume () const { return v_volume; }
private:
virtual float v_volume () const = 0; // returns the volume of the object
};
class Cube final : public Object
{
public:
float width, height, depth;
private:
float v_volume () const override { return width * height * depth; }
};
This allows you to do two things.
- All subclasses of
Object
are required to overridev_volume
to be used. - If you have an
Object*
or anObject&
, you can callvolume
without knowing what type of object it is.
And it comes with some minimal costs:
- Any call to
v_volume
has to go through the vtable, which costs a little performance. This also prevents inlining.
But this is C++. We like having our cake, and eating it too. So is there a way we can get some of the benefits without any of the costs? Namely, can we enforce child classes to meet an interface, without paying any cost? Lets do the same with CRTP.
template
class Object
{
#define RTHIS() static_cast (this);
public:
float volume () const { return RTHIS()->r_volume(); }
/* we assume child class provides r_volume() */
#undef RTHIS
};
class Cube : public Object
{
public:
float width, height, depth;
private:
/* CRTP function - called by our parent class */
float r_volume () const { return width * height * depth; }
};
So now it acts the same as the other, calling cube.volume()
will give you the volume. So what have we gained?
- Calls the volume are now inlineable. Since the dispatch is at compile time, there can be zero cost just like calling
r_volume()
normally. - We keep the requirement of meeting the interface.
What have we lost?
- Complexity. CRTP is a little more complex to understand compared to normal inheritance.
- Base class objects. You cannot have an
Object*
which dynamically at runtime calls the correct volume function. It must be able to evalute it at compile time, or else. - Multiple overrides. With virtual, you could have a subclass of
Cube
provide its ownv_volume()
overriding the one inCube
. You cannot do this in CRTP, unless you inherit fromObject
again, or turnCube
inself into a template. Which makes things even messier. - Error messages are a bit worse. With virtuals its an easy 'you didnt override this function' error. With CRTP it becomes 'this template is written wrong!' because the compiler can't tell who was supposed to fill it in.
CRTP definitely is a lot more complex then normal virtuals, and is more limited where it can be useful. However, if you're dealing with performance critical code and don't care about the features you lose, CRTP is invaluable. In Wolf for example, PointerInterface
is a CRTP class that defines how a pointer must act and is required to be 0 cost compared to a normal pointer. CRTP is a perfect case for this.