Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
289 views
in Technique[技术] by (71.8m points)

c++ - 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.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

(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()
";
  }

  // 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()
";
  }

};

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?


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...