Until C++20 CRTP was the standard workaround to realize mixins in C++. A mixin serves the avoidance of code duplication. It is a kind of compile-time polymorphism.
A typical example are iterator support interfaces. Many of the functions are implemented absolutely identically. For example, C::const_iterator C::cbegin() const
always calls C::const_iterator C::begin() const
.
Note: In C++ struct
is the same as class
, except members and inheritance are public by default.
struct C {
using const_iterator = /* C specific type */;
const_iterator begin() const {
return /* C specific implementation */;
}
const_iterator cbegin() const {
return begin(); // same code in every iterable class
}
};
C++ does not yet provide direct support for such default implementations. However, when cbegin()
is moved to a base class B
, it has no type information about the derived class C
.
struct B {
// ???: No information about C!
??? cbegin() const {
return ???.begin();
}
};
struct C: B {
using const_iterator = /* C specific type */;
const_iterator begin() const {
return /* C specific implementation */;
}
};
Starting from C++23: explicit this
The compiler knows the concrete data type of the object at compile time, until C++20 there was simply no way to get this information.
Since C++23 you can use explicit this (P0847) to get it.
struct B {
// 1. Compiler can deduce return type from implementation
// 2. Compiler can deduce derived objects type by explicit this
decltype(auto) cbegin(this auto const& self) {
return self.begin();
}
};
struct C: B {
using const_iterator = /* C specific type */;
const_iterator begin() const {
return /* C specific implementation */;
}
};
This type of mixin is easy to implement, easy to understand, easy to use, and robust against typos! It is superior to classic CRTP in every respect.
Historical workaround until C++20: CRTP
With CRTP, you passed the data type of the derived class as a tempalte argument to the base class. Thus, basically the same implementation was possible, but the syntax is much more difficult to understand.
// This was CRTP used until C++20!
template <typename T>
struct B {
// Compiler can deduce return type from implementation
decltype(auto) cbegin() const {
// We trust that T is the actual class of the current object
return static_cast<T const&>(*this).begin();
}
};
struct C: B<C> {
using const_iterator = /* C specific type */;
const_iterator begin() const {
return /* C specific implementation */;
}
};
CRTP was complicated and error prone
Moreover, a really nasty typo could quickly happen here. I'll modify the example a bit to make the consequences of the mistake more obvious.
#include <iostream>
struct C;
struct D;
template <typename T>
struct B {
decltype(auto) cget() const {
return static_cast<T const&>(*this).get();
}
};
struct C: B<C> {
short port = 80;
short get() const {
return port;
}
};
// Copy & Paste BUG: should be `struct D: B<**D**>`
struct D: B<C> {
float pi = 3.14159265359f;
float get() const {
return pi;
}
};
int main () {
D d;
// compiles fine, but calles C::get which interprets D::pi as short
std::cout << "Value: " << d.cget() << '\n';
// prints 'Value: 4059' on my computer
}
That is a very dangerous error, because the compiler cannot detect it!