37

同じ問題に対して2つの解決策があります.1つの「コントローラー」から使用されたオブジェクトへのある種のコールバックを作成するため、何を選択すればよいかわかりません。

解決策 1: インターフェイスを使用する

struct AInterface
{
    virtual void f() = 0;
};

struct A : public AInterface
{
    void f(){std::cout<<"A::f()"<<std::endl;}
};

struct UseAInterface
{
    UseAInterface(AInterface* a) : _a(a){}
    void f(){_a->f();}

    AInterface* _a;
};

解決策 2: テンプレートを使用する

struct A
{
    void f(){std::cout<<"A::f()"<<std::endl;}
};

template<class T>
struct UseA
{
    UseA(T* a) : _a(a){}
    void f(){_a->f();}

    T* _a;
};

これは、私の問題を説明するための単純なサンプルです。現実の世界では、インターフェイスにはいくつかの関数があり、1 つのクラスが複数のインターフェイスを実装する場合があります (実装する予定です!)。

コードは外部プロジェクトのライブラリとして使用されず、テンプレートの実装を非表示にする必要はありません。「コントローラー」の実装を非表示にする必要がある場合は、最初のケースの方が適しているためです。

それぞれの場合のメリット・デメリットと、どちらが使いやすいか教えてください。

4

7 に答える 7

57

私の意見では、理由がわかるまで、パフォーマンスは無視する必要があります (実際にはそうではありませんが、マイクロ最適化は無視する必要があります)。いくつかの厳しい要件がなければ (これはほとんどの CPU を使用するタイトなループにあり、インターフェイス メンバー関数の実際の実装は非常に小さいです...) 違いに気付くことは、不可能ではないにしても非常に困難です。

そのため、より高い設計レベルに焦点を当てます。UseAで使用されるすべてのタイプが共通のベースを共有することは理にかなっていますか? 彼らは本当に関連していますか?タイプ間に明確なis-a関係がありますか? 次に、OO アプローチが機能する可能性があります。それらは無関係ですか?つまり、いくつかの特徴を共有していますが、モデル化できる直接的なis-a関係はありませんか? テンプレートアプローチに進みます。

テンプレートの主な利点は、特定の正確な継承階層に準拠していない型を使用できることです。たとえば、コピー構築可能 (C++11 では移動構築可能) なベクターには何でも格納できますが、 anintと aCarは実際にはまったく関係がありません。このようにして、タイプで使用される異なるタイプ間の結合を減らしますUseA

テンプレートの欠点の 1 つは、各テンプレートのインスタンス化が異なるタイプであり、同じ基本テンプレートから生成された残りのテンプレートのインスタンス化とは無関係であることです。これは、同じコンテナ内にUseA<A>格納できないことを意味し、コードの肥大化(両方ともバイナリで生成されます)、コンパイル時間の延長 (追加の関数を考慮しなくても、使用する 2 つの翻訳単位は両方とも同じ関数を生成します) 、リンカはそのうちの 1 つを破棄する必要があります)。UseA<B>UseA<int>::fooUseA<double>::fooUseA<int>::foo

他の回答が主張するパフォーマンスに関しては、何とか正しいですが、ほとんどが重要な点を見逃しています。動的ディスパッチよりもテンプレートを選択する主な利点は、動的ディスパッチの余分なオーバーヘッドではなく、小さな関数をコンパイラによってインライン化できるという事実です (関数定義自体が表示されている場合)。

関数がインライン化されていない場合、関数の実行にほんの数サイクルしかかからない場合を除き、関数の全体的なコストは、動的ディスパッチの余分なコスト (つまり、呼び出しでの余分な間接化と、thisその場合のポインターの可能なオフセット) よりも優先されます。多重/仮想継承の)。関数が実際の作業を行う場合、および/またはインライン化できない場合、それらのパフォーマンスは同じになります。

このコードが 80% CPU 時間の 20% 未満しか使用しないコード、およびこの特定のコード片が CPU の 1% を使用すると言います (これは、パフォーマンスが顕著であるために関数自体がちょうど1 つか 2 つのサイクル!) では、1 時間のプログラム実行のうち約 30 秒を話していることになります。前提をもう一度確認すると、2 GHz の CPU では、時間の 1% は、関数を毎秒 1000 万回以上呼び出す必要があることを意味します。

上記のすべては手を振っており、他の回答とは反対の方向に落ちています (つまり、いくつかの不正確さがあるため、違いが実際よりも小さいように見える可能性がありますが、現実は実際よりもこれに近いです)一般的な回答に対して、動的ディスパッチはコードを遅くします

于 2013-05-16T12:37:17.640 に答える
18

テンプレートの場合は、仮想呼び出しが含まれていないため、パフォーマンスがわずかに向上します。コールバックが非常に頻繁に使用される場合は、テンプレート ソリューションを優先してください。「非常に頻繁に」は、1 秒あたり数千人が関与するまで、おそらく後でさえも実際には開始されないことに注意してください。

一方、テンプレートはヘッダー ファイルにある必要があります。つまり、テンプレートに変更を加えるたびに、それを呼び出すすべてのサイトが強制的に再コンパイルされます。これは、実装が .cpp にあり、必要な唯一のファイルであるインターフェイス シナリオとは異なります。再コンパイル。

于 2013-05-16T11:48:31.120 に答える
1

あなたが説明する選択は、静的ポリモーフィズムと動的ポリモーフィズムの間の選択です。検索すると、このトピックに関する多くの議論が見つかります。

このような一般的な質問に具体的な答えを出すことは困難です。一般に、静的ポリモーフィズムによりパフォーマンスが向上する可能性がありますが、C++11 標準にコンセプトがないということは、クラスが必要なコンセプトをモデル化していない場合に、興味深いコンパイラ エラー メッセージが表示される可能性があることも意味します。

于 2013-05-16T11:51:59.133 に答える
0

私はテンプレートバージョンを使用します。これをパフォーマンスの観点から考えると、それは理にかなっています。

仮想インターフェイス - 仮想を使用すると、メソッドのメモリが動的になり、実行時に決定されます。これには、メモリ内でそのメソッドを見つけるために vlookup テーブルを参照する必要があるというオーバーヘッドがあります。

テンプレート - 静的マッピングを取得します。これは、メソッドが呼び出されたときにルックアップ テーブルを参照する必要がなく、メモリ内のメソッドの場所を既に認識していることを意味します。

パフォーマンスに関心がある場合は、ほとんどの場合、テンプレートを選択することをお勧めします。

于 2013-05-16T11:49:33.747 に答える