web-dev-qa-db-ja.com

C ++ですべての機能を仮想として使用しないのはなぜですか?

仮想関数には、メソッドを呼び出すために間接参照するオーバーヘッドがあることを私は知っています。しかし、現代の建築速度では、それはほとんど無視できると思います。

  1. C++のすべての関数がJavaのように仮想ではない特別な理由はありますか?
  2. 私の知る限り、基本クラスで仮想関数を定義するだけで十分/必要です。親クラスを作成するときに、どのメソッドがオーバーライドされるかわからない場合があります。つまり、子クラスを作成しているときに、誰かが親クラスを編集する必要があるということです。これは不便に聞こえ、時には不可能ですか?

更新:
以下のJonSkeetの回答からの要約:

これは、機能を継承していることを明示的に誰かに認識させることの間のトレードオフです[それ自体に潜在的なリスクがあります[(Jonの応答を確認してください)] [そして潜在的な小さなパフォーマンスの向上] トレードオフあり柔軟性、より多くのコード変更、およびより急な学習曲線。

異なる回答からの他の理由:

インライン化は実行時に行う必要があるため、仮想関数をインライン化することはできません。これは、関数がインライン化の恩恵を受けると予想される場合にパフォーマンスに影響を与えます。

他の理由が考えられるかもしれませんが、私はそれらを知り、要約したいと思います。

53
codeObserver

どのメソッドがパフォーマンスを超えて仮想であるかを制御するのには十分な理由があります。私は実際にはほとんどのメソッドをJavaでファイナルにすることはしませんが、おそらく...メソッドが設計オーバーライドされない限り、おそらくそうすべきではありません。仮想IMO。

継承の設計には注意が必要な場合があります。特に、継承と呼ばれるものと呼ばれるものについて、はるかに多くのドキュメントを作成する必要があることを意味します。 2つの仮想メソッドがあり、一方が他方を呼び出す場合を想像してみてください必須文書化されていない場合、誰かが「呼び出し」メソッドを呼び出す実装で「呼び出された」メソッドをオーバーライドし、無意識のうちにスタックを作成する可能性がありますオーバーフロー(または末尾呼び出しの最適化がある場合は無限ループ)。その時点で、実装の柔軟性が低下します。後日、実装を切り替えることはできません。

C#はさまざまな点でJavaに似た言語ですが、デフォルトでメソッドを非仮想にすることを選択したことに注意してください。他の人はこれに熱心ではありませんが、私は確かにそれを歓迎します-そして私は実際には、クラスもデフォルトで継承できないことを望んでいます。

基本的に、それはJosh Blochからのこのアドバイスに帰着します:継承のための設計またはそれを禁止します。

75
Jon Skeet
  1. C++の主な原則の1つは、使用した分だけ支払うことです(「オーバーヘッドゼロの原則」)。動的ディスパッチメカニズムが必要ない場合は、そのオーバーヘッドを支払う必要はありません。

  2. 基本クラスの作成者は、オーバーライドを許可するメソッドを決定する必要があります。両方を書いている場合は、先に進んで必要なものをリファクタリングしてください。ただし、基本クラスの作成者がその使用を制御する方法が必要なため、このように機能します。

50
eran

しかし、現代の建築速度では、それはほとんど無視できると思います。

この仮定は間違っており、おそらく、この決定の主な理由です。

インライン化の場合を考えてみましょう。 C++のsort関数は、コンパレータ引数をインライン化できるのに対し、Cは(関数ポインタを使用しているため)できないため、一部のシナリオでは、Cのqsortよりもmuch高速に実行されます。極端な場合、これは700%ものパフォーマンスの違いを意味する可能性があります(Scott Meyers、Effective STL)。

同じことが仮想関数にも当てはまります。以前にも同様の議論がありました。たとえば、 C、Perl、Pythonなどの代わりにC++を使用する理由はありますか?

28
Konrad Rudolph

ほとんどの回答は仮想関数のオーバーヘッドを扱っていますが、クラス内のany関数を仮想化しない理由は他にもあります。 standard-layoutからnon-standard-layoutまでのクラス)、バイナリデータをシリアル化する必要がある場合は問題になる可能性があります。これは、C#では異なる方法で解決されます。たとえば、structsをclassesとは異なるタイプのファミリーにすることです。

設計の観点から、すべてのパブリック関数はタイプとタイプのユーザーとの間にコントラクトを確立し、すべての仮想関数(パブリックかどうか)はタイプを拡張するクラスとの異なるコントラクトを確立します。署名するそのような契約の数が多いほど、変更の余地は少なくなります。実際のところ、クライアントへの妥協は拡張機能に必要な妥協とは異なる可能性があるため、パブリックインターフェイスに仮想関数を含めてはならないことを擁護する有名なライターを含むかなりの数の人々がいます。つまり、パブリックインターフェイスはクライアントに対して何をするかを示し、仮想インターフェイスは他の人がそれを行うのにどのように役立つかを示します。

仮想関数のもう1つの効果は、(呼び出しを明示的に修飾しない限り)常に最終的なオーバーライドにディスパッチされることです。つまり、不変条件を維持するために必要な関数(プライベート変数の状態を考えてください)は仮想であってはなりません。 :クラスがそれを拡張する場合、親に明示的に修飾されたコールバックを行う必要があります。そうしないと、レベルで不変条件が壊れます。

これは、@ Jon Skeetが言及した無限ループ/スタックオーバーフローの例と似ていますが、異なる方法で:関数が適切なタイミング。そして、それはつまり、カプセル化を破っていて、leaking抽象化があることを意味します:内部の詳細がインターフェースの一部になりました(ドキュメント+要件拡張機能)、および必要に応じて変更することはできません。

次に、パフォーマンスがあります...パフォーマンスに影響がありますが、ほとんどの場合、それは過大評価されており、パフォーマンスが重要であるいくつかのケースでのみ、フォールバックして関数を非仮想として宣言すると主張できます。 。繰り返しになりますが、2つのインターフェイス(パブリック+拡張機能)はすでにバインドされているため、ビルドされた製品では簡単ではない可能性があります。

あなたは一つのことを忘れます。オーバーヘッドもメモリ内にあります。つまり、オブジェクトごとに仮想テーブルとそのテーブルへのポインタを追加します。ここで、かなりの数のインスタンスが予想されるオブジェクトがある場合、それは無視できません。たとえば、ミリオンインスタンスは4メガバイトに相当します。単純なアプリケーションの場合はそれほど多くはありませんが、ルーターなどのリアルタイムデバイスの場合はこれが重要であることに同意します。

7
roni

私はここでのパーティーにかなり遅れているので、他の回答でカバーされていることに気づかなかったものを1つ追加し、簡単に要約します...

  • 共有メモリでのユーザビリティ:仮想ディスパッチの一般的な実装には、各オブジェクトのクラス固有の仮想ディスパッチテーブルへのポインタがあります。これらのポインタのアドレスは、それらを作成するプロセスに固有です。つまり、共有メモリ内のオブジェクトにアクセスするマルチプロセスシステムは、別のプロセスのオブジェクトを使用してディスパッチできません。高性能マルチプロセスシステムにおける共有メモリの重要性を考えると、これは容認できない制限です。

  • カプセル化:クラス設計者がクライアントコードによってアクセスされるメンバーを制御し、クラスのセマンティクスと不変条件が維持されるようにする機能。たとえば、_std::string_から派生した場合(あえて; -Pを提案するためにいくつかのコメントが表示される場合があります)、通常の挿入/消去/追加操作をすべて使用できます。関数に不正な位置の値を渡すなど、_std::string_に対して常に未定義の動作を行うと、_std::string_データは正常になります。コードをチェックまたは保守している人は、それらの操作の意味を変更したかどうかをチェックする必要はありません。クラスの場合、カプセル化により、クライアントコードを壊すことなく、後で実装を自由に変更できます。同じステートメントの別の見方:クライアントコードは、実装の詳細に依存することなく、クラスを好きなように使用できます。派生クラスで関数を変更できる場合、そのカプセル化メカニズム全体が単純に吹き飛ばされます。

    • 非表示の依存関係:オーバーライドしている関数に依存している他の関数がわからない場合、または関数がオーバーライドされるように設計されている場合は、変更の影響について推論することはできません。たとえば、「私はいつもこれが欲しかった」と考え、std::string::operator[]()at()を変更して、負の値(署名された型キャストの後)がから後方にオフセットされていると見なします。文字列の終わり。しかし、おそらく他の関数は、挿入または削除を試みる前に、インデックスが有効であるという一種のアサーションとしてat()を使用していました-そうでなければスローされることを知っています...そのコードは、未定義の(しかしおそらく致命的な)振る舞いをするための標準指定の方法。
    • ドキュメント:関数virtualを作成することにより、ドキュメントが意図されたカスタマイズポイントであることを示します。 、およびクライアントコードが使用するAPIの一部。
  • インライン化-コード側とCPU使用率:仮想ディスパッチは、関数呼び出しをインライン化するタイミングを計算するコンパイラの作業を複雑にするため、スペース/肥大化とCPU使用率の両方の点でより悪いコードを提供する可能性があります。

  • 呼び出し中の間接参照:いずれかの方法でオフライン呼び出しが行われている場合でも、仮想ディスパッチのパフォーマンスコストはわずかであり、パフォーマンスが重要なシステムで簡単な関数を繰り返し呼び出す場合に重要になる可能性があります。 (仮想ディスパッチテーブルへのオブジェクトごとのポインタを読み取る必要があります。次に、仮想ディスパッチテーブルエントリ自体-VDTページもキャッシュを消費していることを意味します。)

  • メモリ使用量:仮想ディスパッチテーブルへのオブジェクトごとのポインタは、特に小さなオブジェクトの配列の場合、かなりの無駄なメモリを表す可能性があります。これは、キャッシュに収まるオブジェクトが少なくなり、パフォーマンスに大きな影響を与える可能性があることを意味します。

  • メモリレイアウト:C++は、ネットワークまたはさまざまなライブラリやプロトコルのデータ標準で指定されたメンバーデータの正確なメモリレイアウトでクラスを定義できるため、パフォーマンスに不可欠であり、相互運用性に非常に便利です。そのデータは多くの場合、C++プログラムの外部から取得され、別の言語で生成される場合があります。このような通信およびストレージプロトコルには、仮想ディスパッチテーブルへのポインターの「ギャップ」がありません。前述のように、ギャップがあったとしても、コンパイラーを使用すると、受信データにプロセスの正しいポインターを効率的に挿入できます。データへのマルチプロセスアクセス。粗雑だが実用的なポインタ/サイズベースのシリアル化/逆シリアル化/ commsコードもより複雑になり、潜在的に遅くなります。

5
Tony Delroy

使用ごとに支払う (Bjarne Stroustrupの言葉で)。

5
iammilind

この質問にはいくつかの答えがあるようです 仮想関数を過度に使用しないでください-なぜですか? 。私の意見では、目立つのは、継承で何ができるかを知るという点で、複雑さが増すだけだということです。

3
Kevin Jalbert

はい、パフォーマンスのオーバーヘッドが原因です。仮想メソッドは、仮想テーブルと間接参照を使用して呼び出されます。

In Javaすべてのメソッドは仮想であり、オーバーヘッドも存在します。ただし、C++とは異なり、JITコンパイラは実行時にコードをプロファイリングし、使用しないメソッドをインライン化できます。つまり、JVMは、それが本当に必要な場所と、自分で決定を下す必要がない場所を認識しています。

2
Rekin

問題は、Javaは仮想マシン上で実行されるコードにコンパイルされますが、C++に対して同じ保証を行うことはできないということです。Cのより組織化された代替としてC++を使用するのが一般的です。 CはAssemblyに対して1:1の変換を行います。

世界の10個のマイクロプロセッサのうち9個がパーソナルコンピュータやスマートフォンにないことを考えると、この低レベルのアクセスを必要とするプロセッサがたくさんあることをさらに考えると、問題が発生します。

C++は、必要がない場合にその隠れた差分を回避するように設計されているため、1:1の性質を維持します。最初のC++コードのいくつかは、実際には、C-to-Assemblyコンパイラーを実行する前にCに変換される中間ステップを持っていました。

1
Ape-inago