私は同じ問題に対して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つのクラスが複数のインターフェースを実装する場合があります(実装します)。
コードは外部プロジェクトのライブラリとして使用されず、テンプレートの実装を非表示にする必要はありません。「コントローラ」の実装を非表示にする必要がある場合は、最初のケースの方が良いので、これを言います。
それぞれの場合の長所と短所を教えていただけますか?
私の意見では、その理由がない限り、パフォーマンスは無視されます(実際にはそうではありませんが、マイクロ最適化が必要です)である必要があります。いくつかのハード要件がない場合(これは、ほとんどのCPUを使用するタイトループにあり、インターフェイスメンバー関数の実際の実装は非常に小さい...)違いに気づくことは不可能ではないにしても、非常に困難です。
したがって、私はより高い設計レベルに焦点を当てます。 UseA
で使用されるすべての型が共通のベースを共有することは理にかなっていますか?それらは本当に関連していますか?タイプ間に明確なis-a関係はありますか?次に、OOアプローチが機能する可能性があります。それらは無関係です?つまり、いくつかの特性を共有していますが、モデル化できる直接的なis-a関係はありませんか?Goテンプレートアプローチのために。
テンプレートの主な利点は、特定の正確な継承階層に準拠しない型を使用できることです。たとえば、コピー構築可能なベクトル(C++ 11では移動構築可能)に何かを格納できますが、int
とCar
は実際にはどのような関係にもありません。このようにして、UseA
タイプで使用される異なるタイプ間の結合を減らします。
テンプレートの欠点の1つは、各テンプレートのインスタンス化が、同じ基本テンプレートから生成された残りのテンプレートのインスタンス化とは無関係な異なるタイプであることです。つまり、同じコンテナ内にUseA<A>
とUseA<B>
を格納することはできません。code-bloat(UseA<int>::foo
とUseA<double>::foo
の両方が生成されます。バイナリでは)、コンパイル時間が長くなります(追加の関数を考慮しなくても、UseA<int>::foo
を使用する2つの変換単位は両方とも同じ関数を生成し、リンカーはそれらの1つを破棄する必要があります)。
他の回答が主張するパフォーマンスに関しては、それらは何とか正しいですが、ほとんどは重要なポイントを逃しています。動的ディスパッチよりもテンプレートを選択する主な利点は、動的ディスパッチの余分なオーバーヘッドではなく、小さな関数がコンパイラーによってインライン化できることです(関数定義自体が表示されている場合)。
関数がインライン化されていない場合、関数の実行にほんの数サイクルしかかからない限り、関数の全体的なコストは、動的ディスパッチの追加コスト(つまり、呼び出しの追加の間接参照とthis
多重継承または仮想継承の場合のポインタ)。関数が実際の作業を行う場合やインライン化できない場合は、同じパフォーマンスになります。
このコードが80%の一部である場合、一方のアプローチともう一方のアプローチのパフォーマンスの違いが測定可能な少数のケースでも(関数は2サイクルしかかかりません。したがって、そのディスパッチにより各関数のコストが2倍になります) CPU時間の20%未満を占めるコード、およびこの特定のコード部分がCPUの1%を占めると言う(これは、パフォーマンスを顕著にするために、関数自体がちょうど1または2サイクル!)次に、1時間のプログラム実行のうち約30秒を話します。前提を再度確認すると、2GHzのCPUでは、時間の1%は、関数が1秒あたり1000万回以上呼び出される必要があることを意味します。
上記はすべて手を振っており、他の回答とは逆の方向に向かっています(つまり、不正確な点があるため、違いが実際よりも小さいかのように見えますが、現実はこれよりも近いです一般的な答え動的ディスパッチはコードを遅くします。
それぞれに長所と短所があります。 C++プログラミング言語 から:
- 実行時の効率が重視される場合は、派生クラスよりもテンプレートを優先してください。
- 再コンパイルせずに新しいバリアントを追加することが重要な場合は、テンプレートよりも派生クラスを優先します。
- 共通のベースを定義できない場合は、派生クラスよりもテンプレートを優先してください。
- 互換性の制約がある組み込みの型と構造が重要な場合は、派生クラスよりもテンプレートを優先します。
ただし、テンプレートには drawbacks があります
- OOインターフェイスを使用するコードは、テンプレートがヘッダーファイルのコード全体を公開するように強制する場合は常に、.cpp/.CCファイルで非表示にできます。
- テンプレートはコードの膨張を引き起こします。
- OOインターフェイスは明示的です。テンプレートパラメータの要件が暗黙的であり、開発者の頭にのみ存在する場合は常にです。
- テンプレートを頻繁に使用すると、コンパイル速度が低下します。
どちらを使用するかは、状況によって、また、好みによって多少異なります。テンプレート化されたコードは、 STLエラー復号化 などのツールにつながる、わかりにくいコンパイルエラーを生成する可能性があります。うまくいけば、コンセプトはすぐに実装されます。
テンプレートケースでは、仮想呼び出しが含まれないため、パフォーマンスがわずかに向上します。コールバックが非常に頻繁に使用される場合は、テンプレートソリューションを使用してください。 「非常に頻繁に」は、おそらくさらに後でさえ、毎秒数千が関与するまで実際には動きません。
一方、テンプレートはヘッダーファイルにある必要があります。つまり、テンプレートを変更するたびに、テンプレートを呼び出すすべてのサイトが強制的に再コンパイルされます。これは、実装が.cppにあり、必要な唯一のファイルであるインターフェイスのシナリオとは異なります。再コンパイル。
あなたは契約のようなインターフェースを考えることができます。それから派生するクラスはすべて、インターフェースのメソッドを実装する必要があります。
一方、テンプレートにはいくつかの制約があります。たとえば、T
テンプレートパラメータにはメソッドf
が必要です。これらの暗黙の要件は慎重に文書化する必要があります。テンプレートに関連するエラーメッセージは非常に混乱する可能性があります。
Boost Concept はコンセプトチェックに使用でき、暗黙のテンプレート要件を理解しやすくします。
あなたが説明する選択は、静的な多態性と動的な多態性の間の選択です。これを検索すると、このトピックに関する多くのディスカッションが見つかります。
そのような一般的な質問に対して具体的な答えを出すのは難しいです。一般に、静的なポリモーフィズムはパフォーマンスを向上させますが、C++ 11標準に概念がないため、クラスが必要な概念をモデル化していない場合にinterestingコンパイラエラーメッセージが表示される可能性もあります。
私はテンプレート版で行きます。これをパフォーマンスの観点から考えると、理にかなっています。
仮想インターフェイス-仮想を使用すると、メソッドのメモリが動的になり、実行時に決定されます。これには、vlookupテーブルを調べてメモリ内でそのメソッドを見つける必要があるという点でオーバーヘッドがあります。
テンプレート-静的マッピングを取得します。これは、メソッドが呼び出されたときに、ルックアップテーブルを調べる必要がなく、メモリ内のメソッドの場所をすでに認識していることを意味します。
パフォーマンスに関心がある場合は、ほとんどの場合、テンプレートが適しています。