CRTP and multilevel inheritance

(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?

Leave a Comment

Hata!: SQLSTATE[HY000] [1045] Access denied for user 'divattrend_liink'@'localhost' (using password: YES)