Using C++23 “Deducing This” to Implement Static Polymorphism
In a previous article (Using the CRTP and C++20 Concepts to Enforce Contracts for Static Polymorphism) we discussed one way to implement Static Polymorphism using the Curiously Recurring Template pattern (CRTP) to create a Base class which uses C++20 concepts to ensure that derived classes conform to the required contract.
In this follow-up article, we improve upon the CRTP pattern to implement static polymorphism by using the “Explicit Object Parameter” or “Deducing This” functionality of C++23. In addition we use C++20 concepts to ensure that derived classes conform to the required contract specified by the base class.
Much of the code shown below requires C++23 to compile. Also the code may not compile even with the newest compilers on certain platforms (e.g. Apple Clang on MacOS). Compiler support can be seen in the Compiler Support for C++23 at cppreference.com (look for the “Explicit Object Parameter” row). Please also note that all of the code below is for illustration purposes only and isn’t production ready or complete.
In C++ when developers refer to polymorphism, they usually mean dynamic polymorphism (sometimes called late binding). An example of a small class hierarchy that implements a single dynamic polymorphic method is shown below:
#include <iostream>
using std::cout;
struct Context {};
class Shape {
public:
virtual void draw(const Context &context) = 0;
};
class Rectangle : public Shape {
public:
void draw(const Context &context) { cout << "Rectangle::Draw\n"; }; // @1
private:
};
class Circle : public Shape {
public:
void draw(const Context &context) { cout << "Circle::Draw\n"; };
};
Note that the = 0 in the base class signifies a pure virtual method which must be overridden in any derived class. Essentially the pure virtual function defines the interface that the derived class must implement. To not implement the interface is a compile time error. For example if we change the return type of Rectangle::draw on line @1 to be a bool so that it no longer matches the interface we will get an error from the compiler.
In some environments where performance is paramount, even the small performance overhead of calling functions via the v-table is unacceptable. In these circumstances sometimes static polymorphism can be used as a substitute. Static polymorphism occurs at compile-time with the help of overloaded functions and templates.
One commonly used mechanism to implement a type of static polymorphism is the Curiously Recurring Template Pattern or CRTP. In CRTP there is a base class template that defines the static interface. The derived classes then become the template argument for the base class template. The member functions of the base class call member functions of the derived classes via a static cast of the template parameter. Run @Compiler Explorer
#include <iostream>
using std::cout;
struct Context {};
template<typename T>
class Shape {
public:
void do_draw(Context context) {
static_cast<T *>(this)->draw(context);
}
};
class Rectangle : public Shape<Rectangle> {
public:
void draw(Context context) { std::cout << "Rectangle::Draw\n"; }
};
class Circle : public Shape<Circle> {
public:
void draw(Context context) { std::cout << "Circle::Draw\n"; }
};
template <typename T>
void draw(T shape, Context context){
shape.do_draw(context);
}
int main() {
Rectangle r1;
draw(r1, Context {});
Circle c1;
draw(c1, Context {});
}
In C++23 there is a new feature officially called “Explicit Object Parameter” or sometimes “Deducing This” (documentation can be found as part of the Function Declaration section on cppreference.com). In summary — a non-static member function can be declared to take as its first parameter an “explicit object parameter”, denoted with the prefixed keyword this. It should be noted that a function with an explicit object parameter has to be a member function and cannot be static or virtual and it cannot have const, volatile or ref qualifiers. It’s also worth noting that when you call a member function with an explicit object parameter you don’t explicitly pass in this as a parameter.
There are several use-cases for using an explicit object parameter (many are documented here: Deducing this (P0847)). The use-case considered here is the ability to simplify the CRTP pattern for implementing static polymorphism. More specifically, by using an explicit object parameter we can remove the need for static_cast (the use of static_cast should be reduced to a minimum wherever possible) and we can eliminate the need for the templated classes altogether and just rely on normal inheritance. This works because in the function do_draw(), the type of Self is Rectangle or Circle and not Shape. This new approach is shown below: Run @Compiler Explorer
Recommended by LinkedIn
#include <iostream>
using std::cout;
struct Context {};
class Shape {
public:
template<typename Self>
void do_draw(this Self&& self, Context context) {
self.draw(context);
}
};
class Rectangle : public Shape {
public:
void draw(Context context) { std::cout << "Rectangle::Draw\n"; } // @1
};
class Circle : public Shape {
public:
void draw(Context context) { std::cout << "Circle::Draw\n"; }
};
template <typename T>
void draw(T& shape, Context context){
shape.do_draw(context);
}
int main() {
Rectangle r1;
draw(r1, Context {});
Circle c1;
draw(c1, Context {});
}
One problem with this approach is that (as described when using CRTP — see previous article) if we change the return type of Rectangle::draw on line @1 to be a bool in the same way that we did for the Dynamic Polymorphic case, then we do not get an error from the compiler. We’d like to change this so we get a similar kind of error to the dynamic polymorphic case using a pure virtual method.
As before, what we need is a mechanism to be able to tell the compiler that the template type T of the Shape class must conform to a certain interface (in this case the correct implementation of the draw() method). One mechanism for doing this is to use C++ Concepts.
To do this we define a concept which requires the definition of the draw()method (including the fact that it takes a Context parameter and is void). In isolation the concept would look like this:
template<typename T>
concept CanDraw = requires(T t, Context context) {
{ t.draw(context) } -> std::same_as<void>;
};
Now we need to modify the Shape base class to indicate that the Template Type T must conform to the concept. This is shown below: Run @Compiler Explorer
#include <iostream>
using std::cout;
struct Context {};
class Shape;
template<typename T>
concept CanDraw = requires(T t, Context context) {
{ t.draw(context) } -> std::same_as<void>;
};
class Shape {
public:
template<CanDraw Self>
void do_draw(this Self&& self, Context context) {
self.draw(context);
}
};
class Rectangle : public Shape {
public:
void draw(Context context) { std::cout << "Rectangle::Draw\n"; } // @1
};
class Circle : public Shape {
public:
void draw(Context context) { std::cout << "Circle::Draw\n"; }
};
template <typename T>
void draw(T& shape, Context context){
shape.do_draw(context);
}
int main() {
Rectangle r1;
draw(r1, Context {});
Circle c1;
draw(c1, Context {});
}
We can confirm that this is the required behaviour by once again changing the return type of Rectangle::draw on line @1 to be a bool in the same way that we did for the Dynamic Polymorphic case. When we do this (Run @Compiler Explorer) we can see that we get the following easy to understand error:
<source>:17:10: note: candidate template ignored: constraints not satisfied [with Self = Rectangle &]
17 | void do_draw(this Self&& self, Context context) {
| ^
<source>:16:14: note: because 'Rectangle &' does not satisfy 'CanDraw'
16 | template<CanDraw Self>
| ^
<source>:11:26: note: because type constraint 'std::same_as<_Bool, void>' was not satisfied:
11 | { t.draw(context) } -> std::same_as<void>;
In summary, we have been able to improve upon the CRTP pattern to implement static polymorphism by using the “Explicit Object Parameter” or “Deducing This” functionality of C++23. In addition we have used C++20 concepts to ensure that derived classes conform to the required contract specified by the base class.