48

C++ は、仮想メカニズムによる動的バインディングをサポートしています。しかし、私が理解しているように、仮想メカニズムはコンパイラの実装の詳細であり、標準は特定のシナリオで発生するべき動作を指定しているだけです。ほとんどのコンパイラは、仮想テーブルと仮想ポインタを介して仮想メカニズムを実装します。これは、仮想ポインタとテーブルの実装の詳細に関するものではありません。私の質問は次のとおりです。

  1. 仮想ポインタと仮想テーブルメカニズム以外の方法で仮想関数の動的ディスパッチを実装するコンパイラはありますか? 私が見た限りでは、ほとんど (G++、Microsoft Visual Studio を読む) は、仮想テーブル、ポインター メカニズムを介して実装しています。実際には、他のコンパイラの実装はまったくありますか?
  2. sizeof仮想関数だけを持つクラスの は、そのthisコンパイラのポインタ (vptr 内部)のサイズになります。仮想ポインターと TBL メカニズム自体がコンパイラーの実装であることを考えると、上記のステートメントは常に正しいのでしょうか?
4

11 に答える 11

22

オブジェクト内の vtable ポインターが常に最も効率的であるとは限りません。別の言語のコンパイラは、同様の理由でオブジェクト内ポインタを使用していましたが、現在は使用していません。代わりに、オブジェクトアドレスを必要なメタデータにマップする別のデータ構造を使用しています。私のシステムでは、これはたまたま使用する形状情報です。ガベージコレクターによって。

この実装は、単一の単純なオブジェクトのストレージのコストが少し高くなりますが、多くのベースを持つ複雑なオブジェクトの場合はより効率的であり、配列の場合は非常に効率的です。これは、配列内のすべてのオブジェクトのマッピング テーブルで必要なエントリが 1 つだけであるためです。私の特定の実装では、オブジェクト内の任意のポイントへのポインターを指定して、メタデータを見つけることもできます。

地球上で最高のデータ構造である Judy 配列を使用しているため、実際の検索は非常に高速であり、ストレージ要件は非常に控えめです。

また、vtable ポインター以外のものを使用する C++ コンパイラーがないことも知っていますが、それが唯一の方法ではありません。実際、ベースを持つクラスの初期化セマンティクスにより、実装が乱雑になります。これは、オブジェクトが構築されるときに、完全な型がシーソーする必要があるためです。これらのセマンティクスの結果として、複雑な mixin オブジェクトは、大量の vtable のセットが生成され、オブジェクトが大きくなり、オブジェクトの初期化が遅くなります。これはおそらく、サブオブジェクトの実行時の型が常に正しいという要件に従わなければならないほど、vtable 手法の結果ではありません。コンストラクターはメソッドではなく、仮想ディスパッチを賢明に使用できないため、実際には構築中にこれを行う正当な理由はありません。デストラクタは実際のメソッドであるため、これは破壊についてはあまり明確ではありません。

于 2010-12-07T20:57:37.190 に答える
7

私の知る限り、すべての C++ 実装は vtable ポインターを使用しますが、オブジェクトに小さな型インデックス (1-2 B) とその後、小さなテーブル ルックアップで vtable と型情報を取得します。

もう 1 つの興味深いアプローチは、BIBOP (http://foldoc.org/BIBOP) (ページの大きな袋) かもしれませんが、C++ では問題があります。アイデア: 同じタイプのオブジェクトをページに配置します。オブジェクト ポインターの下位ビットを単純に and'ing することで、ページの上部にある型記述子 / vtable へのポインターを取得します。(もちろん、スタック上のオブジェクトにはうまく機能しません!)

もう 1 つの方法は、オブジェクト ポインター自体に特定の型タグ/インデックスをエンコードすることです。たとえば、構造上、すべてのオブジェクトが 16 バイトにアラインされている場合、4 つの LSB を使用して 4 ビット タイプのタグをそこに配置できます。(実際には十分ではありません。)または(特に組み込みシステムの場合)アドレスで未使用の上位ビットが保証されている場合は、そこにタグビットを追加し、シフトとマスクでそれらを回復できます。

これらのスキームは両方とも、他の言語の実装にとって興味深い (そして時々使用される) ものですが、C++ では問題があります。(基本クラス) オブジェクトの構築および破棄中にどの基本クラスの仮想関数オーバーライドが呼び出されるかなど、特定の C++ セマンティクスによって、基本クラス ctors/dtors に入るときに変更するオブジェクトに何らかの状態があるモデルに移動します。

Microsoft C++ オブジェクト モデルの実装に関する以前のチュートリアルが興味深いと思われるかもしれません。 http://www.openrce.org/articles/files/jangrayhood.pdf

ハッピーハッキング!

于 2010-12-12T04:42:40.957 に答える
6
  1. vptr/vtable 以外のアプローチを持つ最新のコンパイラはないと思います。実際、単に効率が悪いだけではないものを見つけるのは難しいでしょう。

    ただし、そのアプローチには設計上のトレードオフの余地がかなりあります。おそらく、特に仮想継承の処理方法に関してです。したがって、これを実装定義にすることは理にかなっています。

    この種のものに興味がある場合は、Inside the C++ Object Modelを読むことを強くお勧めします。

  2. sizeof classコンパイラに依存します。移植可能なコードが必要な場合は、何も仮定しないでください。

于 2010-12-04T10:36:49.917 に答える
5

仮想ポインタと仮想テーブルメカニズム以外の方法で仮想メカニズムを実装するコンパイラはありますか?私が見た限りでは(g ++を読んで、Microsoft Visual Studio)、仮想テーブル、ポインターメカニズムを介して実装しています。それで、実際には他のコンパイラ実装はありますか?

私が知っている現在のすべてのコンパイラは、vtableメカニズムを使用しています。

これは、C++が静的に型チェックされるために可能な最適化です。

いくつかのより動的な言語では、代わりに基本クラスチェーンの動的検索があり、オブジェクトの最も派生したクラスから開始して、仮想的に呼び出されるメンバー関数の実装を検索します。たとえば、それが元のSmalltalkでどのように機能したかです。また、C ++標準では、そのような検索が使用されたかのように仮想呼び出しの効果が記述されています。

1990年代のBorland/Turbo Pascalでは、このような動的検索がWindowsAPIの「ウィンドウメッセージ」のハンドラーを見つけるために採用されました。そして、おそらくBorlandC++でも同じだと思います。これは通常のvtableメカニズムに追加され、メッセージハンドラーにのみ使用されていました。

Borland / Turbo C ++で使用された場合(覚えていませんが)、メッセージIDをメッセージハンドラー関数に関連付けることができる言語拡張機能がサポートされていました。

仮想関数のみを持つクラスのsizeofは、そのコンパイラー上のポインター(this内のvptr)のサイズになります。したがって、仮想ptrおよびtblメカニズム自体がコンパイラーの実装であるとすると、上記のステートメントは常に真になりますか?

正式にはありません(vtableメカニズムを想定している場合でも)、コンパイラに依存します。標準はvtableメカニズムを必要としないため、各オブジェクトへのvtableポインターの配置については何も述べていません。また、他のルールにより、コンパイラーは最後にパディング、未使用バイトを自由に追加できます。

しかし実際にはおそらく。;-)

しかし、それはあなたが頼るべきものでも、あなたが頼る必要があるものでもありません。ただし、他の方向では、たとえばABIを定義している場合など、これを要求できます。そうしないと、単にあなたの要件に適合しないコンパイラがあります。

乾杯&hth。、

于 2010-12-07T11:31:29.810 に答える
4

IIRC Eiffel は異なるアプローチを使用し、メソッドのすべてのオーバーライドは、オブジェクト タイプがチェックされるプロローグと同じアドレスでマージおよびコンパイルされます (したがって、すべてのオブジェクトにはタイプ ID が必要ですが、VMT へのポインターではありません)。もちろん、C++ の場合、リンク時に最終関数を作成する必要があります。ただし、このアプローチを使用する C++ コンパイラは知りません。

于 2010-12-12T00:37:34.113 に答える
4

別のスキームを想像しようとして、イットリルの答えに沿って、次のことを思いつきました。私の知る限り、それを使用するコンパイラはありません!

十分に大きな仮想アドレス空間と柔軟な OS メモリ割り当てルーチンがあればnew、固定の重複しないアドレス範囲に異なるタイプのオブジェクトを割り当てることが可能になります。次に、右シフト操作を使用してそのアドレスからオブジェクトのタイプをすばやく推測し、その結果を vtables のテーブルのインデックスに使用して、オブジェクトごとに 1 つの vtable ポインターを節約できます。

一見すると、このスキームはスタック割り当てオブジェクトで問題が発生するように見えるかもしれませんが、これはきれいに処理できます。

  1. (address range, type)コンパイラは、スタック割り当てオブジェクトごとに、オブジェクトの作成時にペアのグローバル配列にレコードを追加し、オブジェクトの破棄時にレコードを削除するコードを追加します。
  2. スタックを構成するアドレス範囲は、ポインターを読み取る多数のサンクを含む単一の vtable にマップされthis、配列をスキャンしてそのアドレスにあるオブジェクトの対応する型 (vptr) を見つけ、ポイントされた vtable で対応するメソッドを呼び出します。に。(つまり、42 番目のサンクは vtable の 42 番目のメソッドを呼び出します。クラスで使用されるほとんどの仮想関数がnである場合、少なくともサンクnが必要です。)

このスキームは、スタックベースのオブジェクトでの仮想メソッド呼び出しに対して、明らかに重要なオーバーヘッド (ルックアップで少なくとも O(log n)) を引き起こします。スタックベースのオブジェクトの配列または構成 (別のオブジェクト内への包含) がない場合は、vptr をオブジェクトの直前のスタックに配置する、より単純で高速なアプローチを使用できます (オブジェクトの一部とは見なされないことに注意してください)。によって測定されるそのサイズには寄与しませんsizeof)。この場合、thunk は単純にsizeof (vptr)fromを減算しthisて使用する正しい vptr を見つけ、以前と同様に転送します。

于 2010-12-12T00:21:35.737 に答える
4

仮想ポインタと仮想テーブルメカニズム以外の方法で仮想メカニズムを実装するコンパイラはありますか? 私が見た限りでは(g ++、Microsoft Visual Studioを読む)、仮想テーブル、ポインターメカニズムを介して実装しています。実際には、他のコンパイラの実装はまったくありますか?

Binary Tree Dispatch について読むと興味深いかもしれませんが、C++ コンパイラが使用していることを私は知りません。何らかの方法で仮想ディスパッチ テーブルの期待を活用することに関心がある場合は、コンパイル時に型がわかっている場合、コンパイラがコンパイル時に仮想関数呼び出しを解決できる場合があるため、テーブルを参照しない場合があることに注意してください。

仮想関数だけを持つクラスの sizeof は、そのコンパイラのポインタ (this 内の vptr) のサイズになります。したがって、仮想 ptr と tbl メカニズム自体がコンパイラの実装であることを考えると、上記で作成したこのステートメントは常に true になりますか?

独自の仮想メンバーを持つ基本クラスも、仮想基本クラスもないと仮定すると、圧倒的に真実である可能性が高くなります。プログラム全体を分析してクラス階層内のメンバーを 1 つだけ明らかにする、コンパイル時のディスパッチに切り替えるなど、別の方法も考えられます。実行時ディスパッチが必要な場合、コンパイラがさらに間接化を導入する理由を想像するのは困難です。それでも、標準は意図的にこれらのことを正確に規定していないため、実装変更されたり、将来変更される可能性があります。

于 2010-12-07T07:44:01.580 に答える
3

C ++/CLIは両方の仮定から逸脱しています。refクラスを定義すると、マシンコードにコンパイルされません。代わりに、コンパイラはそれを.NETマネージコードにコンパイルします。中間言語では、クラスは組み込み機能であり、仮想メソッドのセットは、メソッドテーブルではなく、メタデータで定義されます。

オブジェクトのレイアウトとディスパッチを実装するための具体的な戦略は、VMによって異なります。Monoでは、仮想メソッドを1つだけ含むオブジェクトは、1つのポインターのサイズではありませんが、 MonoObject構造体に2つのポインターが必要です。オブジェクトの同期用の2つ目。これは実装定義であり、知ることもあまり有用ではないため、sizeofはC ++/CLIのrefクラスではサポートされていません。

于 2010-12-09T19:47:24.870 に答える
3
  1. 代替実装を使用するコンパイラについて聞いたことも見たこともありません。vtable が非常に人気がある理由は、それが最も効率的な実装であるだけでなく、最も簡単な設計であり、最も明白な実装でもあるからです。

  2. 使用したいほとんどすべてのコンパイラで、ほぼ確実に真実です。ただし、それは保証されているわけではなく、常に正しいとは限りません。ほとんど常にそうであるにもかかわらず、信頼することはできません。あなたのお気に入りのコンパイラは、ファンシーのために、あなたに言わずに、その配置を変更してサイズを大きくすることもできます。メモリから、デバッグ情報や好きなものを挿入することもできます。

于 2010-12-04T12:05:32.477 に答える
1

最初に、Borland 独自の C++ 拡張である Dynamic Dispatch Virtual Tables (DDVT) について言及されており、それについてはDDISPATC.ZIPという名前のファイルで読むことができます。Borland Pascal には仮想メソッドと動的メソッドの両方があり、Delphi はさらに別の「メッセージ」構文を導入しました。これは動的に似ていますが、メッセージ用です。現時点では、Borland C++ に同じ機能があるかどうかはわかりません。Pascal にも Delphi にも多重継承はなかったので、Borland C++ DDVT は Pascal や Delphi とは異なる可能性があります。

次に、1990 年代とそれより少し前に、さまざまなオブジェクト モデルの実験が行われましたが、Borland は最先端のものではありませんでした。個人的には、IBM SOMobjects のシャットダウンは、私たち全員がまだ苦しんでいる世界に損害を与えたと思います。SOM をシャットダウンする前に、Direct-to-SOM C++ コンパイラを使用した実験が行われました。したがって、メソッドを呼び出す C++ の方法の代わりに、SOM が使用されます。これは多くの点で C++ vtable に似ていますが、いくつかの例外があります。まず、壊れやすい基本クラスの問題を防ぐために、プログラムは vtable 内でオフセットを使用しません。これは、プログラムがこのオフセットを認識していないためです。基本クラスが新しいメソッドを導入すると、変更される可能性があります。代わりに、呼び出し元は、実行時に作成されたサンクを呼び出します。このサンクは、アセンブリ コードにこの知識を持っています。そして、もう1つ違いがあります。C++ では、多重継承を使用すると、オブジェクトに複数の VMT IIRC を含めることができます。

SOM に関連するドキュメント、Release-to-Release Binary Compatibility in SOM があります。Delta/C++Sun OBIなど、私がほとんど知らない他のプロジェクトとの SOM の比較を見つけることができます。それらは、SOM が解決する問題のサブセットを解決します。また、そうすることで、呼び出しコードもいくらか微調整されます。

私は最近、Visual Age C++ v3.5 for Windows コンパイラ フラグメントを見つけて、実行して実際に操作するのに十分なものを見つけました。ほとんどのユーザーは、DTS C++ で遊ぶためだけに OS/2 VM を入手する可能性は低いですが、Windows コンパイラを使用することはまったく別の問題です。VAC v3.5 は、Direct-to-SOM C++ 機能をサポートする最初で最後のバージョンです。VAC v3.6.5 および v4.0 は適切ではありません。

  1. IBM FTP からVAC 3.5 フィックスパック 9をダウンロードします。このフィックスパックには多くのファイルが含まれているため、完全なコンパイラーを使用する必要さえありません (私は 3.5.7 ディストリビューションを持っていますが、フィックスパック 9 はいくつかのテストを行うのに十分な大きさでした)。
  2. C:\home\OCTAGRAM\DTS などに解凍します。
  3. コマンドラインを開始し、そこで後続のコマンドを実行します
  4. 実行: set SOMBASE=C:\home\OCTAGRAM\DTS\ibmcppw
  5. 実行: C:\home\OCTAGRAM\DTS\ibmcppw\bin\SOMENV.BAT
  6. 実行: cd C:\home\OCTAGRAM\DTS\ibmcppw\samples\compiler\dts
  7. 実行: nmake clean
  8. 実行: nmake
  9. hhmain.exe とその dll は別のディレクトリにあるため、何らかの方法で相互に検出できるようにする必要があります。いろいろ実験していたので「set PATH=%PATH%;C:\home\OCTAGRAM\DTS\ibmcppw\samples\compiler\dts\xhmain\dtsdll」を1回実行しましたが、hhmainの近くにdllをコピーするだけでOKです。 EXE
  10. 実行: hhmain.exe

私はこの方法で出力を得ました:

Local anInfo->x = 5
Local anInfo->_get_x() = 5
Local anInfo->y = A
Local anInfo->_get_y() = B
{An instance of class info at address 0092E318

}
于 2014-12-07T15:38:22.160 に答える
0

Tony Dの答えは、コンパイラがプログラム全体の分析を使用して、仮想関数呼び出しを一意の可能な関数実装への静的呼び出しに置き換えることが許可されていることを正しく指摘しています。obj->method()または同等のものにコンパイルする

if (auto frobj = dynamic_cast<FrequentlyOccurringType>(obj)) {
    frobj->FrequentlyOccurringType::method();  // static dispatch on hot path
} else {
    obj->method();  // vtable dispatch on cold path
}

Karel Driesenと Urs Hölzle は 1996 年に、典型的な C++ アプリケーションでのプログラム全体の完全な最適化の効果をシミュレートした非常に魅力的な論文を書きました。(PDF は、Google で検索すると無料で入手できます。) 残念ながら、彼らは vtable ディスパッチと完全な静的ディスパッチのみをベンチマークしました。彼らはそれを二分木ディスパッチと比較しませんでした。

彼ら、多重継承をサポートする言語 (C++ など) について話している場合、実際には 2 種類の vtable があることを指摘しました。多重継承では、2 番目の基本クラスから継承された仮想メソッドを呼び出すときに、オブジェクト ポインターを "修正" して、2 番目の基本クラスのインスタンスを指すようにする必要があります。このフィックスアップ オフセットは、vtable にデータとして格納することも、コードとして "サンク" に格納することもできます。(詳しくは紙面をご覧ください。)

最近の適切なコンパイラはすべてサンクを使用していると思いますが、その市場浸透率が 100% に達するまでには 10 年から 20 年かかりました。

于 2013-04-25T22:22:59.187 に答える