Implicit interfaces, concepts and compile time polymorphism

​ In the last blog post I shared some of the basic things I learned about static/compile time polymorphism, implicit interfaces and concepts. To summarize, define the following concept for an arbitrary shape (e.g. a rectangle or a square):

template <typename T>
concept ShapeConcept = requires (T t) 
{
    { t.area() } -> std::convertible_to<double>;
};

Then an example of compile time polymorphism is the following function that computes whether the area of a given shape is greater than 10.0:

template <ShapeConcept Shape>
bool is_area_greater_than_10(const Shape& s) 
{
	return s.area() > 10.0;
}

This function can take in an object s of any class as long as the concept is satisfied. We don’t need to use virtual functions and the different classes might have completely different interfaces.

Observe how the above example is different from the following example of dynamic or run time polymorphism, which requires the base class Shape to have a virtual method double area() that is overriden by the child classes:

bool is_area_greater_than_10(const Shape& s) 
{
	return s.area() > 10.0;
}

Curiously recursive template pattern (CRTP)

CRTP is officially described as an idiom/technique to achieve “compile time polymorphism”. However, it’s actually more of a way to inherit from a base class without using virtual functions, maintaining the benefits of inheritance such as code reuse and a common interface. Compile time polymorphism itself is still achieved by using templates as shown above.

Let’s define the Shape base class that is templated on a template type parameter ConcreteShape. ConcreteShape is the child class that will inherit from the parent Shape class:

template <typename ConcreteShape>
struct Shape 
{
    double area() {
       // ...
    }
};

For example, ConcreteShape might be a Square or a Rectangle:

struct Rectangle : public Shape<Rectangle> 
{
    // ...
};

struct Square : public Shape<Square> 
{
    // ...
};

As you can see, the base class now has access to the derived class. Let’s provide the area calculation method for the two derived classes in a separate method area_impl() to not confuse with the base class area() method:

struct Rectangle : public Shape<Rectangle> 
{
    double area_impl() 
    {
        return 8.0;  // Hardcoded for simplicity
    }
};

struct Square : public Shape<Square> 
{
    double area_impl() 
    {
        return 12.0;  // Hardcoded for simplicity
    }
};

Finally, we can use the following trick to call the correct area_impl() version depending on the concrete implementation:

template <typename ConcreteShape>
struct Shape {
    double area() 
    {
       return (static_cast<ConcreteShape*>(this))->area_impl();
    }
};

If ConcreteShape == Rectangle, then at compile time static_cast<ConcreteShape*>(this)) will evaluate to Rectangle* and Rectangle::area_impl() will be called:

Rectangle r{};
r.area();  // 8.0

Square s{};
s.area(); // 12.0

Unfortunately, you cannot have something like std::vector<Shape*> because Shape is not a valid class. Hence, as mentioned earlier, CRTP is not really a way to enable static polymorphism, but rather a way to have code reuse, fixed inheritance and no virtual calls. Two ways exist to achieve compile time polymorphism with “CRTP classes”. One we have already seen before, here CRTP plays no role here as we’d pass the derived object:

template <typename Shape>
bool is_area_greater_than_10(const Shape& s) 
{
	return s.area() > 10.0;
}

And the other way, enabled by CRTP, reminds more of traditional polymorphism:

template <typename ConcreteShape>
bool is_area_greater_than_10(const Shape<ConcreteShape>& s) 
{
	return s.area() > 10.0;
}

Practical usage of CRTP: mixins

In practice, CRTP is often used to implement mixins. Mixins are classes that provide additional, usually shared/generic, functionality to other classes. Consider two simple classes (CRTP is not used here):

struct Rectangle { double area() { return 8.0; } };
struct Square { double area() { return 12.0; } };

Suppose the result of double area() is given in squared meters (m^2), but we want it in squared centimeters (cm^2). You could provide new functions Rectangle::area_cm() and Square::area_cm(), but that’s a lot of duplicate code that might also be useful for other classes that implement some sort of area calculation.

A better way is to provide a mixin using CRTP. Create the following class for unit conversions, assuming that double area() gives area in m^2:

template <typename TypeWithAreaMethod>
struct AreaUnitConversion
{
    double area_m()
    {
        return (static_cast<TypeWithAreaMethod*>(this))->area();
    }

    double area_cm()
    {
        return (static_cast<TypeWithAreaMethod*>(this))->area() * 100.0 * 100.0;
    }
};

We can now augment the classes with this new unit conversion capability:

struct Rectangle : public AreaUnitConversion<Rectangle> 
{ 
    double area() { return 8.0; } 
};

Rectangle r{};
r.area();  // 8.0
r.area_m();  // 8.0
r.area_cm();  //  80,000.0

Square s{};
s.area();  // 8.0
//s.area_m();  // Compile time error: Square has no member area_m()
//s.area_cm();  //  Compile time error: Square has no member area_cm()

Limitations of CRTP: multiple-level inheritence

Not a straightforward task. For e.g. see this stackoverflow thread.

CRTP in C++23

The trick to cast this* to the correct type in (static_cast<ConcreteShape*>(this))->area_impl() is quite verbose and not easy to read at first glance. In C++23, we can instead deduce the type using a new feature, which doesn’t require us to template the base class Shape on the derived class ConcreteShape:

struct Shape {
    double area(this auto&& self) 
    {
       return self.area_impl();
    }
};

struct Rectangle : Shape { /*...*/ };
struct Square : Shape { /*...*/ };

Here self is deduced to be the object that’s actually calling the function (of type ConcreteShape). For more information, see this blog post. To summarize:

“Usually when we call an object’s member function, the object is implicitly passed to the member function, despite not being present in the parameter list. P0847 allows us to make this parameter explicit, giving it a name and const/reference qualifiers.”