Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CRTP and multilevel inheritance

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.

like image 446
Cassio Neri Avatar asked Aug 11 '13 17:08

Cassio Neri


People also ask

What is difference between hierarchical and multilevel inheritance?

Summary – Multiple vs Multilevel Inheritance The Single Level Inheritance has one base class and one derived class. Hierarchical Inheritance has one base class and many derived classes. The Hybrid Inheritance is a combination of Multilevel and Multiple Inheritance.

What is the point of Crtp?

CRTP may be used to implement "compile-time polymorphism", when a base class exposes an interface, and derived classes implement such interface.

What is multilevel inheritance with example?

What is multilevel Inheritance in java? Multilevel Inheritance in java occurs when a class extends a class that extends another class. This is called multilevel Inheritance in java. For example, class C extends class B, and class B extends class A.

What are the multilevel and hybrid inheritance?

The hybrid inheritance in C++ is also called multipath inheritance, where one derived class can inherit properties of the base class in different paths. Sometimes also called multipath inheritance. For example, it can be achieved with a combination of both multilevel and hierarchical inheritance.


1 Answers

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

like image 124
Cassio Neri Avatar answered Sep 22 '22 20:09

Cassio Neri