C++23 adds the deducing this feature, which is a complete game changer when it comes to CRTP multilevel inheritance.
With this feature, a multilevel-CRTP hierarchy looks as simple as:
struct level0
{
auto foo(this auto self) const { return self; };
};
struct level1 : level0 {};
struct level2 : level1 {};
Compare all the other answers here in this thread, including mine. Not only that some are not functional, they are much, much more complicated than the above.
As another bonus point, deducing this is even more consistent than the previous version. For example, compare it with standard multilevel CRTP,
template<typename derived_t>
struct level0_impl
{
auto foo() const { return static_cast<derived_t const&>(*this); };
};
struct level0 : public level0_impl<level0> { /* using level0::level0 and so on...*/};
template<typename derived_t> struct level1_impl : level0_impl<derived_t> {};
struct level1 : public level1_impl<level1> {};
template<typename derived_t> struct level2_impl : level1_impl<derived_t> {};
struct level2 : public level2_impl<level2> {};
That is not quite dumb -- I have a concrete hierarchy along the _impl
classes, which I can use to avoid a lot of boilerplate in the derived classes, and moreover I can use any level on its own with the non-impl classes (-- as is the case in normal dynamical inheritance).
However, if I call this snippet,
auto f = [](level1 const& l1) { return l1.foo(); };
auto var = f(level2());
what will it give? Yes, a compile time-error, because level2
and level1
are not related. level1
derives from level1_impl<level1>
and level2
from level1_impl<level2>
, so yep, they're not related.
Ok, we're smart, so let's first change the function parameter to a level1_impl
, which basically maps it back to a type in the actual hierarchy.
auto f = []<typename T>(level1_impl<T> const& l1) { return l1.foo(); };
auto var = f(level2());
What is now the type of var
? Should be something around level1
, or not? -- that is at least what could be expected in the dynamic inheritance setting. Nope, here it's a level2
. That is, because the foo()
function in the base class always returns the final type of the hierarchy, which is not what one always wants.
Particularly if you have defined functions on several layers and the return types should fit together -- that is pain with "classical" multilevel CRTP. You end up either working always with the final type, or with duplicating each function on any hierarchy level -- which is exactly what CRTP is intended to avoid.
DEMO
Enter deducing this: now everything just works nicely and as expected. level1
and level2
are related by normal inheritance, and the previous lambda will return a level1
:
struct level0
{
auto foo(this auto const& self) { return self; };
};
struct level1 : level0 {};
struct level2 : level1 {};
auto f = [](level1 const& l1) { return l1.foo(); };
auto var = f(level2());
static_assert(std::is_same_v<decltype(var), level1>);
DEMO
Did I mention that deducing this is a complete game changer for multilevel CRTP inheritance? Basically, it makes this pattern practically available for the first time, because the previous approaches are un-maintainable and way to complex. Thanks @barry and tartanllama, and coworkers for your proposal, this is really a great extension of the language!
B
derived fromA<Derived>
instead ofA<B<Derived>>
(as it's usual in CRTP) seems to save some boilerplate code because the call fromA
goes directly to the most derived class and the compiler goes up the hierarchy to find the deepestfoo
. (Contrast this with my solution where we go down one level at time.) Having said that, there's an issue:C c; B<C>& b = c; b.foo();
callsB::foo
even though the most derived type ofb
isC
which is a diversion from what polymorphism would do. +1 anyway! – Nacred