29

私の友人は、「CRTP を使用してマルチレベル継承のポリモーフィズムを置き換える方法」と尋ねました。より正確には、次のような状況で:

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 */ }
}

私の友人と私は、CRTP がポリモーフィズムのドロップイン代替ではないことを理解していますが、両方のパターンを使用できる場合に関心があります。(この質問のために、各パターンの長所と短所には関心がありません。)

  1. この質問は以前に尋ねられましたが、作成者は名前付きパラメーターのイディオムを実装したいと考えており、彼自身の回答は CRTP よりもこの問題に重点を置いていることが判明しました。一方、最も投票数の多い回答は、基本クラスで同名を呼び出す派生クラス メソッドに関するもののようです。

  2. 私は非常に多くのボイラープレート コードを含む回答 (以下に投稿) を思いつきました。

4

7 に答える 7

17

(1)階層の最上位クラスは次のようになります。

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()「デフォルトの実装」がなく、呼び出しが派生クラスに向けられるという意味で、純粋仮想メソッドと同様に動作します。A<T>ただし、これは非基本クラスとしてインスタンス化されることを妨げません。この動作を取得するにA<T>::~A()protected.

注意: 残念ながら、GCC のバグにより、使用時に特別なメンバー関数が public になり= default;ます。この場合、1 つを使用する必要があります。

protected:
    ~A() {}

それでも、デストラクタを保護するだけでは、コンストラクタへの呼び出しがデストラクタへの呼び出しと一致しない場合には十分ではありません (これは を介し​​て発生する可能性がありoperator newます)。したがって、すべてのコンストラクター (コピー コンストラクターとムーブ コンストラクターを含む) も保護することをお勧めします。

のインスタンス化A<T>が許可されA<T>::foo()、非純粋仮想メソッドのように動作する必要がある場合は、以下Aのテンプレート クラスに似ている必要がありBます。

(2)階層の中間 (または上記の段落で説明したように、最上位のクラス) のクラスは次のようになります。

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

};

コンストラクタとデストラクタは public で、Tデフォルトはvoidです。これにより、タイプのオブジェクトをB<>階層内で最も派生したものにすることができ、これが合法になります。

B<> b;
b.foo();

が最も派生したクラスである場合 (または、より正確には が である場合) 、 「 の既定の実装」 (を出力する) を呼び出すB<T>::foo()という意味で、 は非純粋仮想メソッドとして動作することにも注意してください。でない場合、呼び出しは派生クラスに向けられます。これは、タグのディスパッチによって実現されます。B<T>Tvoidb.foo(); foo()B::foo()Tvoid

&B::foo == &T::foo無限再帰呼び出しを避けるためにテストが必要です。実際、派生クラスTが を再実装しない場合foo()、呼び出しは再びwhich 呼び出しstatic_cast<const T*>(this)->foo();に解決されます。さらに、このテストはコンパイル時に解決でき、コードは または のいずれかであり、オプティマイザーはテストを完全に削除できます (例: -O3 を指定した GCC)。B::foo()B::foo_impl(std::false_type)if (true)if (false)

(3)最後に、階層の最下部は次のようになります。

class C : public B<C> {

public:

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

};

または、継承された実装 ( ) が適切でC::foo()あれば、完全に削除することもできます。B<C>::foo()

C::foo()それを呼び出しても派生クラス (存在する場合) への呼び出しをリダイレクトしないという意味で、これは final メソッドに似ていることに注意してください。(非最終的にするには、次のようなテンプレート クラスをB使用する必要があります。)

(4)以下も参照:

CRTP の使用中にエラーを回避するには?

于 2013-08-11T17:02:51.103 に答える
4

私の元の答えが実際には手元にある最終的なオーバーライドの質問を扱っていないことに気付いた後、私はそれに追加しようと思いました。以前の回答と同様の方法で「最終的なオーバーライド」ソリューションを考え出したかったのです。

問題:

CRTP インターフェイス クラスは常に、静的キャストを介して最上位の派生クラスにリダイレクトします。これは、「最終」関数の概念と矛盾しています。目的の「最終」関数が最上位の派生クラスで実装されておらず、上位クラスによって「オーバーライド」されている場合 (関数に「最終」を与えることができないため) CRTPで回避しようとしている仮想でない限り、CRTPインターフェイスは目的の「最終」機能ではなく「オーバーライド」にリダイレクトします。

ソリューション:

インターフェイスを 3 つの概念に分割します。

  • 以下を継承するリダイレクト関数を持たない抽象インターフェイス クラス:
  • リダイレクト関数が最上位の派生クラスにリダイレクトされる抽象リダイレクトクラス
  • 具体的な「リダイレクト オーバーライド」クラス。リダイレクト関数を実装でオーバーライドします。

具象実装クラスをインスタンス化するとき、すべての「継承可能な実装クラス」を介して具象実装クラスをテンプレート パラメータとしてインターフェイスに渡す代わりに、インターフェイスがテンプレート パラメータとして継承するリダイレクト クラスを渡します。

関数を「final」にしたい場合は、抽象的なリダイレクト クラスを継承する「リダイレクト オーバーライド クラス」を作成し、final にしたいリダイレクト関数をオーバーライドします。次に、継承可能なすべての実装クラスを通じて、この新しい「リダイレクト オーバーライド クラス」をパラメーターとして渡します。

このアプローチでは:

  1. 「最終」関数は、キャストを介してリダイレクトされるのではなく、直接呼び出されます (「最終」関数をリダイレクト オーバーライド クラスではなく、継承可能な実装クラスで実装する必要がある場合を除きます)。
  2. 「最終」関数は、将来のユーザー コードによってオーバーライドできません。
  3. 各「最終」関数は、継承のレベルごとに追加の ImplFinal クラスのみを必要とし、追加のボイラープレートは必要ありません。

これはすべて非常に複雑に聞こえるので、理解しやすくするために作成したフロー図を次に示します。

DImpl および EImpl には、DImpl または EImpl のいずれかが継承されている場合にオーバーライドできない最終関数があります。

コード例:

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

コードは以下を出力します。

### 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()
于 2016-09-01T21:18:49.700 に答える
1

マルチレベルの継承は問題ではありませんが、CRTP がどのようにポリモーフィズムを作成するかが問題です。

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

derived各呼び出しでの冗長な型キャストを回避する方法を導入することもできます。

template<typename Derived>
struct Base
{
    Derived& derived() { return *static_cast<Derived*>(this); }
    const Derived& derived() const { return *static_cast<const Derived*>(this); }
};
于 2016-10-01T17:08:28.577 に答える
0

クラス内のボイラープレート コードを削減できる実装例を次に示します (ただし、補助コードの総量は削減できません)。

このソリューションのアイデアは、SFINAE とオーバーロードを使用して impl 関数を選択することです。

(i) クラス A

template <typename T> class A {
   void foo() const {
    static_cast<const T*>(this)->foo( Type2Type<T> );
   }
 }

TypetoType はテンプレート構造体です

template< typename T > struct Type2Type {
  typedef T OriginalType
}

これは、コンパイラが foo() impl を選択するのに役立ちます。過負荷によって。

(i) クラス B

template <typename T = void>
class B : public A<B<T>> { 

 void foo(...) { 
   std::cout << "B::foo()\n";  
 }
 void foo( Type2Type< std::enable_if< is_base_of<T,B>::value == true, B>::type> ) { 
   static_cast<const T*>(this)->foo( Type2Type<T> );
 }
}

ここで、階層の最下位が C で指定されている場合、TypetoType 引数は必要ありません。

(ii) クラス C

class C : public B<C> { 
 void foo(...) { 
   std::cout << "C::foo()\n";  
 }
}

T==B の場合、std::is_base_of が true を返すことはわかっています。ここでは、2 つのテンプレート引数が同じ場合に false_type を返す独自のバージョンの is_base_of を使用します。何かのようなもの

template<class B, class D>
struct is_base_of : public is_convertible<D *, B *> {};
template<class B, class B>
struct is_base_of<B, B> :  public false_type {};
template<class D>
struct is_base_of<void, D> : public false_type {};

結論: std::enable_if が失敗した場合、foo() の可変引数バージョンが (SFINAE のため) 利用可能な唯一のものになり、コンパイラーは foo の B バージョンを実装します。ただし、enable_if が失敗しない場合、コンパイラは 2 番目のバージョンを選択します (コンパイラがオーバーロード関数間の最適な一致を見つけようとするとき、可変長引数が最後のオプションであるため)。

于 2013-08-11T23:10:16.677 に答える