動的ポリモーフィズムを回避するCRTP


回答:


139

2つの方法があります。

1つ目は、型の構造に対して静的にインターフェイスを指定する方法です。

template <class Derived>
struct base {
  void foo() {
    static_cast<Derived *>(this)->foo();
  };
};

struct my_type : base<my_type> {
  void foo(); // required to compile.
};

struct your_type : base<your_type> {
  void foo(); // required to compile.
};

2番目の方法は、base-to-baseまたはpointer-to-baseイディオムの使用を避け、コンパイル時に配線を行うことです。上記の定義を使用して、次のようなテンプレート関数を作成できます。

template <class T> // T is deduced at compile-time
void bar(base<T> & obj) {
  obj.foo(); // will do static dispatch
}

struct not_derived_from_base { }; // notice, not derived from base

// ...
my_type my_instance;
your_type your_instance;
not_derived_from_base invalid_instance;
bar(my_instance); // will call my_instance.foo()
bar(your_instance); // will call your_instance.foo()
bar(invalid_instance); // compile error, cannot deduce correct overload

したがって、関数内で構造/インターフェース定義とコンパイル時の型の演繹を組み合わせると、動的ディスパッチの代わりに静的ディスパッチを行うことができます。これが静的ポリモーフィズムの本質です。


15
すばらしい答え
Eli Bendersky

5
私は強調したいnot_derived_from_baseから派生していないbase、またそれが由来しているbase...
leftaroundabout

3
実際には、my_type / your_type内のfoo()の宣言は必要ありません。codepad.org/ylpEm1up(スタックオーバーフローの原因)-コンパイル時にfooの定義を強制する方法はありますか?-わかりました、解決策を見つけました:ideone.com/C6Oz9-多分あなたはあなたの答えでそれを修正したいでしょう。
cooky451 2012

3
この例でCRTPを使用する動機は何ですか?barがtemplate <class T>として定義される場合、void bar(T&obj){obj.foo(); 次に、fooを提供するクラスであれば問題ありません。したがって、あなたの例に基づくと、CRTPの唯一の用途はコンパイル時にインターフェースを指定することです。それは何のためですか?
アントンダネイコ2013

1
@Dean Michael確かに、fooがmy_typeとyour_typeで定義されていなくても、例のコードはコンパイルされます。これらのオーバーライドがない場合、base :: fooは再帰的に呼び出されます(およびstackoverflows)。だから、多分あなたはcooky451が示したようにあなたの答えを修正したいですか?
アントンダネイコ2013

18

私自身、CRTPについてのきちんとした議論を探していました。Todd Veldhuizen's Techniques for Scientific C ++は、これ(1.3)や、式テンプレートのような他の多くの高度なテクニックに最適なリソースです。

また、GoogleブックでCoplienのオリジナルのC ++ Gems記事のほとんどを読むことができることもわかりました。多分それはまだ事実です。


@fizzer私はあなたが提案する部分を読みましたが、それでもテンプレート<class T_leaftype> double sum(Matrix <T_leaftype>&A);が何をするか理解していません。template <class Whatever> double sum(Whatever&A);と比較してあなたを買います。
アントンダネイコ2013

@AntonDaneyko基本インスタンスで呼び出されると、基本クラスの合計が呼び出されます。たとえば、正方形であるかのようにデフォルトの実装で「形状の領域」が呼び出されます。この場合のCRTPの目標は、派生動作が必要になるまで台形を形状として参照しながら、最も派生した実装である「台形の領域」などを解決することです。基本的に、通常はdynamic_cast仮想メソッドが必要になるときはいつでも。
John P


-5

このウィキペディアの回答には、必要なものがすべて含まれています。すなわち:

template <class Derived> struct Base
{
    void interface()
    {
        // ...
        static_cast<Derived*>(this)->implementation();
        // ...
    }

    static void static_func()
    {
        // ...
        Derived::static_sub_func();
        // ...
    }
};

struct Derived : Base<Derived>
{
    void implementation();
    static void static_sub_func();
};

これが実際にどれだけあなたを買うかはわかりませんが。仮想関数呼び出しのオーバーヘッドは(もちろんコンパイラに依存します):

  • メモリ:仮想関数ごとに1つの関数ポインター
  • ランタイム:1つの関数ポインター呼び出し

CRTP静的ポリモーフィズムのオーバーヘッドは次のとおりです。

  • メモリ:テンプレートのインスタンス化ごとのベースの複製
  • ランタイム:1つの関数ポインター呼び出し+ static_castが実行していること

4
実際には、テンプレートのインスタンス化ごとのBaseの複製は幻想です。これは、(vtableがまだない限り)コンパイラがbaseのストレージと派生したものを1つの構造体にマージするためです。関数ポインターの呼び出しもコンパイラーによって最適化されます(static_cast部分)。
ディーン・マイケル

19
ところで、CRTPの分析は正しくありません。それは:メモリ:何もない、ディーン・マイケルが言ったように。ランタイム:仮想ではなく1つの(より速い)静的関数呼び出し。これが演習の全体のポイントです。static_castは何もせず、コードのコンパイルを許可するだけです。
Frederik Slijkerman、2008年

2
私のポイントは、ベースコードがすべてのテンプレートインスタンスで複製されることです(あなたが話しているのは非常にマージです)。テンプレートパラメータに依存するメソッドを1つだけ持つテンプレートを持つのと同じです。それ以外の場合は、基本クラスの方が優れています。それ以外の場合は、複数回(「マージ」)プルされます。
user23167 2008年

1
ベース内の各メソッドは、派生ごとに再度コンパイルされます。(Derivedのプロパティが異なるため)インスタンス化された各メソッドが異なる(予想される)場合、必ずしもオーバーヘッドとしてカウントできません。ただし、(通常の)基本クラスの複雑なメソッドがサブクラスの仮想メソッドを呼び出す場合と比較して、全体的なコードサイズが大きくなる可能性があります。また、実際には<Derived>にまったく依存しないユーティリティメソッドをBase <Derived>に配置した場合も、インスタンス化されます。多分グローバルな最適化はそれを幾分修正するでしょう。
greggo 2016年

CRTPのいくつかの層を通過する呼び出しは、コンパイル中にメモリ内で拡張されますが、TCOとインライン化によって簡単に縮小できます。では、CRTP自体が原因ではありませんよね?
John P
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.