CRTP and multilevel inheritance
Asked Answered
N

7

36

A friend of mine asked me "how to use CRTP to replace polymorphism in a multilevel inheritance". More precisely, in a situation like this:

struct A {

  void bar() {
    // do something and then call foo (possibly) in the derived class:
    foo();
  }

  // possibly non pure virtual
  virtual void foo() const = 0;
}

struct B : A {
  void foo() const override { /* do something */ }
}

struct C : B {
  // possibly absent to not override B::foo().
  void foo() const final { /* do something else */ }
}

My friend and I understand that CRTP is not a drop-in replacement for polymorphism but we are interested in cases where both patterns can be used. (For the sake of this question, we are not interested in pros and cons of each pattern.)

  1. This question has been asked before but it turned out the the author wanted to implement the named parameter idiom and his own answer focus on this problem more than on the CRTP. On the other hand, the most voted answer seems to be just about a derived class method calling its homonym in the base class.

  2. I came up with an answer (posted below) which has quite a lot of boilerplate code and I wonder if there are simpler alternatives.

Nacred answered 11/8, 2013 at 17:2 Comment(0)
N
21

(1) The topmost class in the hierarchy looks like:

template <typename T>
class A {

public:

  void bar() const {
    // do something and then call foo (possibly) in the derived class:
    foo();
  }

  void foo() const {
    static_cast<const T*>(this)->foo();
  }

protected:

  ~A() = default;

  // Constructors should be protected as well.

};

A<T>::foo() behaves similarly to a pure virtual method in the sense that it doesn't have a "default implementation" and calls are directed to derived classes. However, this doesn't prevent A<T> from being instantiated as a non base class. To get this behavior A<T>::~A() is made protected.

Remark: Unfortunately a GCC bug turns special member functions public when = default; is used. In this case, one should used

protected:
    ~A() {}

Still, protecting the destructor is not enough for the cases where a call to a constructor is not matched by a call to the destructor (this might happen via operator new). Hence, it's advisable to protect all constructors (including copy- and move-constructor) as well.

When instantiations of A<T> should be allowed and A<T>::foo() should behave like a non-pure virtual method, then A should be similar to the template class B below.

(2) Classes in the middle of the hierarchy (or the topmost one, as explained in the paragraph above) look like:

template <typename T = void>
class B : public A<B<T>> { // no inherinace if this is the topmost class

public:

  // Constructors and destructor

  // boilerplate code :-(
  void foo() const {
    foo_impl(std::is_same<T, void>{});
  }

private:

  void foo_impl(std::true_type) const {
    std::cout << "B::foo()\n";
  }

  // boilerplate code :-(
  void foo_impl(std::false_type) const {
    if (&B::foo == &T::foo)
      foo_impl(std::true_type{});
    else
      static_cast<const T*>(this)->foo();
  }

};

Constructors and destructors are public and T defaults to void. This allows objects of type B<> to be the most derived in the hierarchy and makes this legal:

B<> b;
b.foo();

Notice also that B<T>::foo() behaves as a non pure virtual method in the sense that, if B<T> is the most derived class (or, more precisely, if T is void), then b.foo(); calls the "default implementation of foo()" (which outputs B::foo()). If T is not void, then the call is directed to the derived class. This is accomplished through tag dispatching.

The test &B::foo == &T::foo is required to avoid an infinite recursive call. Indeed, if the derived class, T, doesn't reimplement foo(), the call static_cast<const T*>(this)->foo(); will resolve to B::foo() which calls B::foo_impl(std::false_type) again. Furthermore, this test can be resolved at compile time and the code is either if (true) or if (false) and the optimizer can remove the test altogether (e.g. GCC with -O3).

(3) Finally, the bottom of the hierarchy looks like:

class C : public B<C> {

public:

  void foo() const {
    std::cout << "C::foo()\n";
  }

};

Alternatively, one can remove C::foo() entirely if the inherited implementation (B<C>::foo()) is adequate.

Notice that C::foo() is similar to a final method in the sense that calling it does not redirected the call to a derived class (if any). (To make it non final, a template class like B should be used.)

(4) See also:

How to avoid errors while using CRTP?

Nacred answered 11/8, 2013 at 17:2 Comment(0)
M
17

Note: This isn't specifically a solution to the "final override" problem, but to the CRTP multi-level inheritance problem in general (since I haven't found an answer anywhere on how to do it, and I think my findings would be useful).

EDIT: I've posted a solution to the final override problem here

I recently learned of CRTP and its potential as a static replacement for runtime polymorphism. After searching for a while to see whether CRTP could be used as a like-for-like "drop-in" replacement for polymorphism, such that you could use multi-level inheritance and the like, I have to say, I was pretty surprised that I couldn't find a proper generic solution anywhere without boilerplate which could scale indefinitely. After all, why not try to make CRTP a drop-in replacement for polymorphism, given all of its performance benefits? Some investigation ensued, and here's what I came up with:

The problem:

The classic CRTP pattern creates a "loop" of accessibility between the CRTP interface and the implementation class. (The CRTP interface class has access to the "base" implementation class via a static cast of itself to the template parameter type, and the implementation class inherits the public interface from the CRTP interface class.) When you create a concrete implementation, you are closing the loop, making it very difficult to inherit from the concrete implementation class, such that whatever derives from it also behaves in a polymorphic manner.

Classic CRTP single-level inheritance

The solution:

Separate the pattern into three concepts:

  • The "abstract interface class", i.e. the CRTP interface.
  • The "inheritable implementation class", which can be indefinitely inherited from by other inheritable implementation classes.
  • The "concrete class", which combines the abstract interface with the desired inheritable implementation class, and closes the loop.

Here's a diagram to help illustrate:

Multiple-level inheritance with CRTP

The trick is to pass the concrete implementation class as the template parameter all the way down through all of the inheritable implementation classes into the abstract interface class.

With this approach you can:

  1. inherit implementations indefinitely,
  2. call the highest implemented method in a CRTP multi-level inheritance chain from any level,
  3. design each implementation in an hierarchy agnostic manner,
  4. forget having to use boilerplate code (well, no more than with classic single-level CRTP anyway),

which reflects virtual/runtime polymorphism perfectly.

Example code:

#include <iostream>

template <class Top>
struct CrtpInterface
{
  void foo()
  {
    std::cout << "Calling CrtpInterface::foo()\n";
    fooImpl();
  }
  void foo2()
  {
    std::cout << "Calling CrtpInterface::foo2()\n";
    fooImpl2();
  }
  void foo3()
  {
    std::cout << "Calling CrtpInterface::foo3()\n";
    fooImpl3();
  }
  void foo4()
  {
    std::cout << "Calling CrtpInterface::foo4()\n";
    fooImpl4();
  }

// The "pure virtual functions"
protected:
  inline void fooImpl()
  {
    top().fooImpl();
  }
  inline void fooImpl2()
  {
    top().fooImpl2();
  }
  inline void fooImpl3()
  {
    top().fooImpl3();
  }
  inline void fooImpl4()
  {
    top().fooImpl4();
  }
  inline Top& top()
  {
    return static_cast<Top&>(*this);
  }
};

template<class Top>
class DefaultImpl : public CrtpInterface<Top>
{
  using impl = CrtpInterface<Top>;
  friend impl;

  void fooImpl()
  {
    std::cout << "Default::fooImpl()\n";
  }

  void fooImpl2()
  {
    std::cout << "Default::fooImpl2()\n";
    std::cout << "Calling foo() from interface\n";
    impl::foo();
  }

  void fooImpl3()
  {
    std::cout << "Default::fooImpl3()\n";
    std::cout << "Calling highest level fooImpl2() from interface\n";
    impl::fooImpl2();
  }

  void fooImpl4()
  {
    std::cout << "Default::fooImpl4()\n";
    std::cout << "Calling highest level fooImpl3() from interface\n";
    impl::fooImpl3();
  }
};

template<class Top>
class AImpl : public DefaultImpl<Top>
{
  using impl = CrtpInterface<Top>;
  friend impl;

  void fooImpl()
  {
    std::cout << "A::fooImpl()\n";
  }
};

struct A : AImpl<A>
{
};

template<class Top>
class BImpl : public AImpl<Top>
{
  using impl = CrtpInterface<Top>;
  friend impl;

  protected:
    BImpl()
      : i{1}
    {
    }

  private:
    int i;
    void fooImpl2()
    {
      std::cout << "B::fooImpl2(): " << i << "\n";
    }
};

struct B : BImpl<B>
{
};

template<class Top>
class CImpl : public BImpl<Top>
{
  using impl = CrtpInterface<Top>;
  friend impl;

  protected:
    CImpl(int x = 2)
      : i{x}
    {
    }

  private:
    int i;
    void fooImpl3()
    {
      std::cout << "C::fooImpl3(): " << i << "\n";
    }
};

struct C : CImpl<C>
{
  C(int i = 9)
    : CImpl(i)
  {
  }
};

template<class Top>
class DImpl : public CImpl<Top>
{
  using impl = CrtpInterface<Top>;
  friend impl;

  void fooImpl4()
  {
    std::cout << "D::fooImpl4()\n";
  }
};

struct D : DImpl<D>
{
};

int main()
{
  std::cout << "### A ###\n";
  A a;
  a.foo();
  a.foo2();
  a.foo3();
  a.foo4();

  std::cout << "### B ###\n";
  B b;
  b.foo();
  b.foo2();
  b.foo3();
  b.foo4();

  std::cout << "### C ###\n";
  C c;
  c.foo();
  c.foo2();
  c.foo3();
  c.foo4();

  std::cout << "### D ###\n";
  D d;
  d.foo();
  d.foo2();
  d.foo3();
  d.foo4();
}

Which prints:

### A ###
Calling CrtpInterface::foo()
A::fooImpl()
Calling CrtpInterface::foo2()
Default::fooImpl2()
Calling foo() from interface
Calling CrtpInterface::foo()
A::fooImpl()
Calling CrtpInterface::foo3()
Default::fooImpl3()
Calling highest level fooImpl2() from interface
Default::fooImpl2()
Calling foo() from interface
Calling CrtpInterface::foo()
A::fooImpl()
Calling CrtpInterface::foo4()
Default::fooImpl4()
Calling highest level fooImpl3() from interface
Default::fooImpl3()
Calling highest level fooImpl2() from interface
Default::fooImpl2()
Calling foo() from interface
Calling CrtpInterface::foo()
A::fooImpl()
### B ###
Calling CrtpInterface::foo()
A::fooImpl()
Calling CrtpInterface::foo2()
B::fooImpl2(): 1
Calling CrtpInterface::foo3()
Default::fooImpl3()
Calling highest level fooImpl2() from interface
B::fooImpl2(): 1
Calling CrtpInterface::foo4()
Default::fooImpl4()
Calling highest level fooImpl3() from interface
Default::fooImpl3()
Calling highest level fooImpl2() from interface
B::fooImpl2(): 1
### C ###
Calling CrtpInterface::foo()
A::fooImpl()
Calling CrtpInterface::foo2()
B::fooImpl2(): 1
Calling CrtpInterface::foo3()
C::fooImpl3(): 9
Calling CrtpInterface::foo4()
Default::fooImpl4()
Calling highest level fooImpl3() from interface
C::fooImpl3(): 9
### D ###
Calling CrtpInterface::foo()
A::fooImpl()
Calling CrtpInterface::foo2()
B::fooImpl2(): 1
Calling CrtpInterface::foo3()
C::fooImpl3(): 2
Calling CrtpInterface::foo4()
D::fooImpl4()

Using this approach, and a "variant-style" wrapper (built using some sechsy variadic templates and macros, maybe I'll post that later), which acted like a pointer to a virtual abstract base class, I was able to effectively create a vector of CRTP classes inheriting from the same interface.

I measured the performance in comparison to a vector of like-for-like virtual classes, all based on an equivalent virtual interface, and I found that with this approach, depending on the scenario, I could achieve a performance increase of up to 8x! This is very encouraging, given the relatively little overhead needed to generate a functionally polymorphic CRTP class hierarchy!

Meeting answered 30/8, 2016 at 20:37 Comment(0)
M
6

After realising that my original answer didn't actually deal with the final override question at hand, I thought I'd add to it. I wanted to come up with a "final override" solution in a similar manner as my previous answer.

The problem:

The CRTP interface class always redirects via a static cast to the highest derived class. This is at odds with the concept of a "final" function: if the desired "final" function is not implemented on the highest derived class, and is "overridden" by a higher class (since you cannot give a function a "final" property unless it is virtual, which we are trying to avoid in CRTP), the CRTP interface will redirect not to the desired "final" function but to the "override".

The solution:

Split the interface into three concepts:

  • The abstract interface class without any of the redirecting functions, which inherits:
  • an abstract redirecting class whose redirecting functions redirect to the highest derived class, unless one or more of the redirecting functions is overridden by:
  • a concrete "redirect override" class, which overrides the redirecting functions with an implementation.

When instantiating the concrete implementation class, instead of passing the concrete implementation class as a template parameter through all of the "inheritable implementation classes" into the interface, we pass the redirecting class which the interface will inherit from as the template parameter.

When we want to make a function "final", we simply create a "redirect override class" which inherits from the abstract redirecting class, and overrides the redirecting function which we want to make final. Then we pass this new "redirect override class" as a parameter through all of the inheritable implementation classes.

With this approach:

  1. "final" functions are called directly rather than redirected via a cast (unless you need the "final" function to be implemented in the inheritable implementation class rather than the redirect override class),
  2. "final" functions cannot be overridden by any future user code,
  3. each "final" function only requires an extra ImplFinal class per level of inheritance, with no extra boilerplate.

This all sounds very complicated, so here is a flow diagram I made in attempt to make things easier to understand:

DImpl and EImpl have final functions which cannot be overridden when either DImpl or EImpl are inherited from:

Example code:

#include <iostream>
#include <type_traits> 

template <class Top>
struct Redirect
{
protected:
  // The "pure virtual functions"
  inline void fooImpl()
  {
    top().fooImpl();
  }
  inline void fooImpl2()
  {
    top().fooImpl2();
  }
  inline void fooImpl3()
  {
    top().fooImpl3();
  }
  inline void fooImpl4()
  {
    top().fooImpl4();
  }
  inline Top& top()
  {
    // GCC doesn't allow static_cast<Top&>(*this) 
    // since Interface uses private inheritance
    static_assert(std::is_base_of<Redirect, Top>::value, "Invalid Top class specified.");
    return (Top&)(*this);
  }
};

// Wraps R around the inner level of a template T, e.g:
// R := Redirect, T := X, then inject_type::type := Redirect<X>
// R := Redirect, T := A<B<C<X>>>, then inject_type::type := A<B<C<Redirect<X>>>>
template<template<class> class R, class T>
struct inject_type
{
  using type = R<T>;
};

template<template<class> class R, class InnerFirst, class... InnerRest, template<class...> class Outer>
struct inject_type<R, Outer<InnerFirst, InnerRest...>>
{
  using type = Outer<typename inject_type<R, InnerFirst>::type, InnerRest...>;
};

// We will be inheriting either Redirect<...> or something 
// which derives from it (and overrides the functions).
// Use private inheritance, so that all polymorphic calls can
// only go through this class (which makes it impossible to 
// subvert redirect overrides using future user code).
template <class V>
struct Interface : private inject_type<Redirect, V>::type
{
  using impl = Interface;

  void foo()
  {
    std::cout << "Calling Interface::foo()\n";
    fooImpl();
  }

  void foo2()
  {
    std::cout << "Calling Interface::foo2()\n";
    fooImpl2();
  }

  void foo3()
  {
    std::cout << "Calling Interface::foo3()\n";
    fooImpl3();
  }

  void foo4()
  {
    std::cout << "Calling Interface::foo4()\n";
    fooImpl4();
  }

private:
  using R = typename inject_type<::Redirect, V>::type;

protected:
  using R::fooImpl;
  using R::fooImpl2;
  using R::fooImpl3;
  using R::fooImpl4;
};

template<class V>
struct DefaultImpl : Interface<V>
{
  template<class>
  friend struct Redirect;

protected:
  // Picking up typename impl from Interface, where all polymorphic calls must pass through
  using impl = typename DefaultImpl::impl;

  void fooImpl()
  {
    std::cout << "Default::fooImpl()\n";
  }

  void fooImpl2()
  {
    std::cout << "Default::fooImpl2()\n";
    std::cout << "Calling foo() from interface\n";
    impl::foo();
  }

  void fooImpl3()
  {
    std::cout << "Default::fooImpl3()\n";
    std::cout << "Calling highest level fooImpl2() from interface\n";
    impl::fooImpl2();
  }

  void fooImpl4()
  {
    std::cout << "Default::fooImpl4()\n";
    std::cout << "Calling highest level fooImpl3() from interface\n";
    impl::fooImpl3();
  }
};

template<class V>
struct AImpl : public DefaultImpl<V>
{
  template<class>
  friend struct Redirect;

protected:
  void fooImpl()
  {
    std::cout << "A::fooImpl()\n";
  }
};

struct A : AImpl<A>
{
};

template<class V>
struct BImpl : public AImpl<V>
{
  template<class>
  friend struct Redirect;

protected:
  BImpl()
    : i{1}
  {
  }

private:
  int i;
  void fooImpl2()
  {
    std::cout << "B::fooImpl2(): " << i << "\n";
  }
};

struct B : BImpl<B>
{
};

template<class V>
struct CImpl : public BImpl<V>
{
  template<class>
  friend struct Redirect;

  protected:
    CImpl(int x = 2)
      : i{x}
    {
    }

  private:
    int i;
    void fooImpl3()
    {
      std::cout << "C::fooImpl3(): " << i << "\n";
    }
};

struct C : CImpl<C>
{
  C(int i = 9)
    : CImpl(i)
  {
  }
};

// Make D::fooImpl4 final
template<class V>
struct DImplFinal : public V
{
protected:
  void fooImpl4()
  {
    std::cout << "DImplFinal::fooImpl4()\n";
  }
};


// Wrapping V with DImplFinal overrides the redirecting functions
template<class V>
struct DImpl : CImpl<DImplFinal<V>>
{
};

struct D : DImpl<D>
{
};

template<class V>
struct EImpl : DImpl<V>
{
  template<class>
  friend struct Redirect;

protected:
  void fooImpl()
  {
    std::cout << "E::fooImpl()\n";
  }

  void fooImpl3()
  {
    std::cout << "E::fooImpl3()\n";
  }

  // This will never be called, because fooImpl4 is final in DImpl
  void fooImpl4()
  {
    std::cout << "E::fooImpl4(): this should never be printed\n";
  }
};

struct E : EImpl<E>
{
};

// Make F::fooImpl3 final
template<class V, class Top>
struct FImplFinal : public V
{
protected:
  // This is implemented in FImpl, so redirect
  void fooImpl3()
  {
    top().fooImpl3();
  }

  // This will never be called, because fooImpl4 is final in DImpl
  void fooImpl4()
  {
    std::cout << "FImplFinal::fooImpl4() this should never be printed\n";
  }

  inline Top& top()
  {
    // GCC won't do a static_cast directly :( 
    static_assert(std::is_base_of<FImplFinal, Top>::value, "Invalid Top class specified");
    return (Top&)(*this);  
  }
};

// Wrapping V with FImplFinal overrides the redirecting functions, but only if they haven't been overridden already
template<class V>
struct FImpl : EImpl<FImplFinal<V, FImpl<V>>>
{
  template<class>
  friend struct Redirect;
  template<class, class>
  friend struct FImplFinal;

protected:
  FImpl() 
    : i{99} 
  {
  }

  // Picking up typename impl from DefaultImpl
  using impl = typename FImpl::impl;

private:
  int i;

  void fooImpl2()
  {
    std::cout << "F::fooImpl2()\n";
    // This will only call DFinal::fooImpl4();
    std::cout << "Calling fooImpl4() polymorphically. (Should not print FImplFinal::fooImpl4() or EImpl::fooImpl4())\n";
    impl::fooImpl4();
  }

  void fooImpl3()
  {
    std::cout << "FImpl::fooImpl3(), i = " << i << '\n';
  }
};

struct F : FImpl<F>
{
};

int main()
{
  std::cout << "### A ###\n";
  A a;
  a.foo();
  a.foo2();
  a.foo3();
  a.foo4();

  std::cout << "### B ###\n";
  B b;
  b.foo();
  b.foo2();
  b.foo3();
  b.foo4();

  std::cout << "### C ###\n";
  C c;
  c.foo();
  c.foo2();
  c.foo3();
  c.foo4();

  std::cout << "### D ###\n";
  D d;
  d.foo();
  d.foo2();
  d.foo3();
  d.foo4();

  std::cout << "### E ###\n";
  E e;
  e.foo();
  e.foo2();
  e.foo3();
  e.foo4();

  std::cout << "### F ###\n";
  F f;
  f.foo();
  f.foo2();
  f.foo3();
  f.foo4();
}

The code prints:

### A ###
Calling CrtpInterface::foo()
A::fooImpl()
Calling CrtpInterface::foo2()
Default::fooImpl2()
Calling foo() from interface
Calling CrtpInterface::foo()
A::fooImpl()
Calling CrtpInterface::foo3()
Default::fooImpl3()
Calling highest level fooImpl2() from interface
Default::fooImpl2()
Calling foo() from interface
Calling CrtpInterface::foo()
A::fooImpl()
Calling CrtpInterface::foo4()
Default::fooImpl4()
Calling highest level fooImpl3() from interface
Default::fooImpl3()
Calling highest level fooImpl2() from interface
Default::fooImpl2()
Calling foo() from interface
Calling CrtpInterface::foo()
A::fooImpl()
### B ###
Calling CrtpInterface::foo()
A::fooImpl()
Calling CrtpInterface::foo2()
B::fooImpl2(): 1
Calling CrtpInterface::foo3()
Default::fooImpl3()
Calling highest level fooImpl2() from interface
B::fooImpl2(): 1
Calling CrtpInterface::foo4()
Default::fooImpl4()
Calling highest level fooImpl3() from interface
Default::fooImpl3()
Calling highest level fooImpl2() from interface
B::fooImpl2(): 1
### C ###
Calling CrtpInterface::foo()
A::fooImpl()
Calling CrtpInterface::foo2()
B::fooImpl2(): 1
Calling CrtpInterface::foo3()
C::fooImpl3(): 9
Calling CrtpInterface::foo4()
Default::fooImpl4()
Calling highest level fooImpl3() from interface
C::fooImpl3(): 9
### D ###
Calling CrtpInterface::foo()
A::fooImpl()
Calling CrtpInterface::foo2()
B::fooImpl2(): 1
Calling CrtpInterface::foo3()
C::fooImpl3(): 2
Calling CrtpInterface::foo4()
DImplFinal::fooImpl4()
### E ###
Calling CrtpInterface::foo()
E::fooImpl()
Calling CrtpInterface::foo2()
B::fooImpl2(): 1
Calling CrtpInterface::foo3()
E::fooImpl3()
Calling CrtpInterface::foo4()
DImplFinal::fooImpl4()
### F ###
Calling CrtpInterface::foo()
E::fooImpl()
Calling CrtpInterface::foo2()
F::fooImpl2()
Attempting to call FFinal::fooImpl4() or E::fooImpl4()
DImplFinal::fooImpl4()
Calling CrtpInterface::foo3()
FImpl::fooImpl3(), i = 99
Calling CrtpInterface::foo4()
DImplFinal::fooImpl4()
Meeting answered 1/9, 2016 at 21:18 Comment(0)
K
4
template<class Derived>
struct A {
  void foo() {
    static_cast<Derived*>(this)->foo();
  }
};

template<class Derived>
struct B: A <Derived> {
  void foo() {
    // do something
  }
};

struct C: B <C> {
  void foo(); // can be either present or absent
};

if foo() in C is absent the one in B will be invoked. Otherwise the one in B will be overridden.

Kielce answered 21/4, 2017 at 18:49 Comment(1)
Beautiful idea: making B derived from A<Derived> instead of A<B<Derived>> (as it's usual in CRTP) seems to save some boilerplate code because the call from A goes directly to the most derived class and the compiler goes up the hierarchy to find the deepest foo. (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(); calls B::foo even though the most derived type of b is C which is a diversion from what polymorphism would do. +1 anyway!Nacred
R
4

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!

Radborne answered 10/2, 2023 at 21:48 Comment(0)
R
2

There is a lot of stuff going on in this thread which I do not find useful, so I share here my own solution to this problem.

CRTP is mainly a pattern for code reduction. In order to work properly, it is necessary that at each level of the inheritance hierachy, one is able to call all functions from the levels below -- just as in usual dynamic inheritance.

However, in CRTP, each stage must further be aware of the final type, which currently derives from it, because in the end this is the whole point of CRTP -- to call functions that always apply to the current (static) final type.

One can obtain this by adding a layer of indirection at each level of the static inheritance hierarchy, as in the following example:

template<typename derived_t>
struct level0_impl
{
    auto const& derived() const { return static_cast<derived_t const&>(*this); }
    auto foo_impl() const { return "level0"; }
    auto foo() const { std::cout << "Hi, I'm "<<derived().foo_impl()<<std::endl; } //should not be overwritten in derived class
};
struct level0 : public level0_impl<level0>
{
    using level0_impl<level0>::level0_impl;
};


template<typename derived_t>
struct level1_impl : level0_impl<derived_t>
{
    auto foo_impl() const { return "level1"; }
};
struct level1 : public level1_impl<level1>
{
    using level1_impl<level1>::level1_impl;
};


template<typename derived_t>
struct level2_impl : public level1_impl<derived_t>
{
    double bar = 2.0;
    auto foo_impl() const { return "level2"; }
    auto only_for_level2_and_derived() const
    {
        return bar;
    };
};
struct level2 : public level2_impl<level2>
{
    using level2_impl<level2>::level2_impl;
};

// ... and so on ...

One can use any of the levels as in the following:

int main()
{
    level1().foo();   //prints "Hi, I'm level 1"
    level2().foo();   //prints "Hi, I'm level 2"

    std::cout<< level2().only_for_level2_and_derived() <<std::endl;   //prints variable bar = 2.0
}

The result is that you have to implement foo() only once in the base class, and use information from the derived classes to specialize it. This works for any number of derived classes, and, if used correctly, can save you thousands of lines of code.

This is a nice thing, which notably does not work with the other approaches in this thread such as

template<typename T> class A { auto& derived() {return static_cast<T&>(*this);} };
template<typename T> class B : A<B<T> > {};
template<typename T> class C : B<C> {};    //here derived() in the base class does 
                                           //not return C, but B<C> -- which is
                                           //not what one usually wants in CRTP
Radborne answered 5/2, 2019 at 22:13 Comment(0)
V
1

Multilevel inheritance is not the problem, but how CRTP creates polymorphism is.

template<typename Derived>
struct Base
{
    void f() { /* Basic case */ }

    // "Pure virtual" method
    void pure() { static_cast<Derived*>(this)->pure(); }
};

struct Overriding: Base<Overriding>
{
    void f() { /* Special case */ }

    // This method must exists to prevent endless recursion in Base::f
    void pure() { /* ... */ }
};

struct NonOverriding: Base<NonOverriding>
{
    void pure() { /* ... */ }
};

template<typename Derived>
void f(const Base<Derived>& base)
{
    base.f();    // Base::f
    base.pure(); // Base::pure, which eventually calls Derived::pure

    // Derived::f if an overriding method exists.
    // Base::f otherwise.
    static_cast<const Derived&>(base).f();
}

One can also introduce a derived method to avoid wordy type casting at each invocation.

template<typename Derived>
struct Base
{
    Derived& derived() { return *static_cast<Derived*>(this); }
    const Derived& derived() const { return *static_cast<const Derived*>(this); }
};
Verla answered 1/10, 2016 at 17:8 Comment(1)
No need for derived(). When the class is instantiated you know Derived* so just save it.Scouting

© 2022 - 2024 — McMap. All rights reserved.