6

C++ では、実行時 (サブクラス、仮想関数) またはコンパイル時 (テンプレート、関数のオーバーロード) ポリモーフィズムのいずれかを使用して同じ機能を実装できる場合、なぜ一方を選択するのでしょうか?

コンパイルされたコードは、コンパイル時のポリモーフィズム (テンプレート型用に作成されたメソッド/クラス定義の数が多い) の場合は大きくなり、コンパイル時は柔軟性が高くなり、実行時は「より安全な」ポリモーフィズムが得られると思います (つまり、誤って誤って使用される可能性があります)。

私の仮定は正しいですか?他にメリット/デメリットはありますか? 両方が実行可能なオプションであるが、どちらか一方が明らかにより良い選択になる特定の例を誰か挙げることができますか?

また、コンパイル時のポリモーフィズムは、vtable を介して関数を呼び出す必要がないため、より高速なコードを生成しますか、それともコンパイラによって最適化されますか?

例:

class Base
{
  virtual void print() = 0;
}

class Derived1 : Base
{
  virtual void print()
  {
     //do something different
  }
}

class Derived2 : Base
{
  virtual void print()
  {
    //do something different
  }
}

//Run time
void print(Base o)
{
  o.print();
}

//Compile time
template<typename T>
print(T o)
{
  o.print();
}
4

3 に答える 3

7

静的ポリモーフィズムは、主に積極的なインライン化の可能性により、より高速なコードを生成します。仮想関数がインライン化されることはめったになく、ほとんどが「非ポリモーフィック」シナリオです。C++ FAQのこの項目を参照してください。速度が目標の場合、基本的に選択の余地はありません。

一方、静的ポリモーフィズムを使用すると、コンパイル時間だけでなく、コードの可読性とデバッグ性も大幅に低下します。例: 抽象メソッドは、特定のインターフェイス メソッドの実装を強制するクリーンな方法です。静的ポリモーフィズムを使用して同じ目標を達成するには、概念チェックまたは不思議なことに繰り返されるテンプレート パターンに戻す必要があります。

動的ポリモーフィズムを実際に使用しなければならない唯一の状況は、コンパイル時に実装が利用できない場合です。たとえば、動的ライブラリからロードされたときです。ただし、実際には、よりクリーンなコードと高速なコンパイルのためにパフォーマンスを交換したい場合があります。

于 2013-06-01T19:43:45.087 に答える
2

C++ では、実行時 (サブクラス、仮想関数) またはコンパイル時 (テンプレート、関数のオーバーロード) ポリモーフィズムのいずれかを使用して同じ機能を実装できる場合、なぜ一方を選択するのでしょうか?

コンパイルされたコードは、コンパイル時のポリモーフィズム(テンプレート型用に作成されたより多くのメソッド/クラス定義)のために大きくなると思います...

多くの場合、はい - テンプレート パラメーターのさまざまな組み合わせに対して複数のインスタンス化が行われるため、次のことを考慮してください。

  • テンプレートでは、実際に呼び出された関数のみがインスタンス化されます
  • デッドコードの排除
  • 一定の配列次元により、メンバー変数T mydata[12];をオブジェクトに割り当てたり、ローカル変数の自動ストレージなどを許可したりできますが、ランタイム ポリモーフィック実装では動的割り当て (つまりnew[]) を使用する必要がある場合があります。これは、場合によってはキャッシュ効率に劇的な影響を与える可能性があります。
  • 関数呼び出しのインライン化。これにより、小さなオブジェクトの get/set 操作などの些細なことを、私がベンチマークした実装で約 1 桁高速化します
  • 仮想ディスパッチを回避します。これは、関数ポインターのテーブルへのポインターをたどり、それらの 1 つに対して行外呼び出しを行うことになります (通常、パフォーマンスを最も損なうのは行外の側面です)。

...そして、そのコンパイル時間により、柔軟性が向上します...

テンプレートは確かに行います:

  • 異なる型に対してインスタンス化された同じテンプレートが与えられた場合、同じコードが異なることを意味するT::f(1)可能性がありvoid f(int) noexceptます。別の観点から見ると、さまざまなパラメーターの型が、テンプレート化されたコードが必要とするものを、最も適した方法で提供できます。virtual void f(double)T::foperator()(float)

  • SFINAEを使用すると、オブジェクトが積極的に推奨を行うことなく、オブジェクトがサポートする最も効率的なインターフェイスを使用するようにコードをコンパイル時に調整できます。

  • 上記のインスタンス化のみの関数呼び出しの側面により、クラス テンプレートの一部の関数のみがコンパイルされる型でクラス テンプレートをインスタンス化することで「逃げる」ことができます。それらの機能は、他のタイプでサポートされTemplate<MyType>ているすべての操作をサポートしているように見えTemplate<>ますが、特定の操作を試みたときに失敗するだけです。Template<>他の方法では、すべての操作に興味がない場合でも使用できるため、それは良いことです

    • Concepts [Lite] が将来の C++ 標準になる場合、プログラマーは、テンプレート パラメーターとして使用される型がサポートしなければならないセマンティック操作に対して、より強力な先行制約を設定するオプションを利用できるようになりますTemplate<MyType>::operationX。一般に、コンパイルの早い段階でより単純なエラーメッセージを表示します

...一方、ランタイムは「より安全な」ポリモーフィズムを提供します (つまり、誤って誤って使用されるのが難しくなります)。

おそらく、上記のテンプレートの柔軟性を考えると、より厳格であるためです。ランタイム ポリモーフィズムの主な「安全性」の問題は次のとおりです。

  • いくつかの問題は、最終的に「太った」インターフェースを奨励することになります (Stroustrup が C++ プログラミング言語で言及している意味で): 一部の派生型に対してのみ機能する関数を含む API、およびアルゴリズム コードは派生型に「問い合わせ」続ける必要があります。あなたのためにこれをしてください」「これを行うことができますか」「それはうまくいきましたか」など.

  • 仮想デストラクタが必要です: 一部のクラスにはそれらがありません (例std::vector) - それらから安全に派生することが難しくなり、仮想ディスパッチ テーブルへのオブジェクト内ポインタがプロセス間で有効でないため、ランタイム ポリモーフィック オブジェクトを配置することが難しくなります。複数のプロセスからアクセスするための共有メモリ内

両方が実行可能なオプションであるが、どちらか一方が明らかにより良い選択になる特定の例を誰か挙げることができますか?

もちろん。たとえば、クイック ソート関数を作成しているとします。仮想比較関数と仮想スワップ関数を使用して、Sortable 基本クラスから派生したデータ型のみをサポートするか、Lessデフォルトでstd::less<T>,とstd::swap<>。並べ替えのパフォーマンスがこれらの比較操作とスワップ操作のパフォーマンスによって圧倒的に支配されることを考えると、テンプレートはこれに非常に適しています。そのため、C++std::sortは C ライブラリのジェネリックqsort関数よりも明らかにパフォーマンスが優れています。ジェネリック関数は、事実上仮想ディスパッチの C 実装である関数ポインターを使用します。詳細については、こちらを参照してください。

また、コンパイル時のポリモーフィズムは、vtable を介して関数を呼び出す必要がないため、より高速なコードを生成しますか、それともコンパイラによって最適化されますか?

多くの場合は高速ですが、非常にまれに、テンプレート コードの肥大化の合計の影響が、コンパイル時のポリモーフィズムが通常は高速である無数の方法を圧倒する可能性があり、バランスをとってみると悪化します。

于 2016-01-15T11:24:02.400 に答える
2

明らかに悪いケースや次善のケースを除外した後は、ほとんど何も残らないと思います。IMO そのような選択に直面しているときはかなりまれです。例を挙げて質問を改善することができます。そのために、実際の比較バンが提供されます。

現実的な選択肢があると仮定すると、私はコンパイル時の解決策を選びます - 絶対に必要ではないもののためにランタイムを無駄にするのはなぜですか? また、コンパイル時に何かを決定する方が考えやすく、頭の中でフォローして評価を行うことができます。

仮想関数は、関数ポインターと同様に、正確なコール グラフを作成できなくなります。下から見直すことはできますが、上から見直すことは容易ではありません。仮想関数はいくつかの規則に従う必要がありますが、そうでない場合は、それらすべてに罪人を探す必要があります。

また、パフォーマンスに多少の損失がありますが、ほとんどの場合、おそらく大したことではありませんが、反対側でバランスが取れていない場合、なぜそれを取るのですか?

于 2013-06-01T19:32:25.283 に答える