以下は GCC に当てはまります (LLVM linkにも当てはまります) が、使用しているコンパイラにも当てはまります。これらはすべて実装依存であり、C++ 標準では管理されていません。ただし、GCC は独自のバイナリ標準ドキュメントItanium ABIを作成します。
C++ での仮想関数のパフォーマンスに関する私の記事の一部として、仮想テーブルがどのようにレイアウトされるかの基本的な概念をより簡単な言葉で説明しようとしました。質問に対する回答は次のとおりです。
オブジェクトの内部表現を表すより正しい方法は次のとおりです。
| vptr | ======= | ======= | <-- your object
|----A----| |
|---------B---------|
B
にはその基本クラス が含まれA
ており、終了後に自分のメンバーをいくつか追加するだけです。
B*
からへのキャストはA*
実際には何もせず、同じポインターを返し、同じvptr
ままです。しかし、一言で言えば、仮想関数は常に vtable 経由で呼び出されるわけではありません。他の関数と同じように呼び出されることもあります。
詳しい説明はこちら。メンバー関数を呼び出す 2 つの方法を区別する必要があります。
A a, *aptr;
a.func(); // the call to A::func() is precompiled!
aptr->A::func(); // ditto
aptr->func(); // calls virtual function through vtable.
// It may be a call to A::func() or B::func().
問題は、関数がどのように呼び出されるかがコンパイル時にわかっていることです: vtable を介して、または単に通常の呼び出しになります。そして問題は、キャスト式の型はコンパイル時に認識されるため、コンパイラはコンパイル時に適切な関数を選択するということです。
B b, *bptr;
static_cast<A>(b)::func(); //calls A::func, because the type
// of static_cast<A>(b) is A!
この場合、vtable の内部も検索しません。
一般的に、いいえ。クラスが複数のベースから継承し、それぞれが独自の vtable を持つ場合、クラスは複数の vtable を持つことができます。このような仮想テーブルのセットは、「仮想テーブル グループ」を形成します (pt. 3 を参照)。
クラスには、複雑なオブジェクトのベースを構築するときに仮想関数を正しく分配するために、一連の構築 vtables も必要です。私がリンクした標準でさらに読むことができます。
これが例です。がおよびC
から継承し、各クラスが 、および、またはその名前に関連する仮想関数を定義していると仮定します。A
B
virtual void func()
a
b
c
にはC
、2 つの vtable の vtable グループがあります。1 つの vtable を共有しA
(現在のクラスの独自の関数が移動する vtable は「プライマリ」と呼ばれます)、vtableB
が追加されます。
| C::func() | a() | c() || C::func() | b() |
|---- vtable for A ----| |---- vtable for B ----|
|--- "primary virtual table" --||- "secondary vtable" -|
|-------------- virtual table group for C -------------|
メモリ内のオブジェクトの表現は、その vtable とほぼ同じように見えます。グループ内のすべての vtable の前に a を追加するvptr
だけで、データがオブジェクト内でどのように配置されているかを大まかに見積もることができます。これについては、GCC バイナリ標準の関連セクションを参照してください。
仮想ベース (一部) は、vtable グループの最後に配置されます。これは、各クラスが仮想ベースを 1 つだけ持つ必要があり、それらが「通常の」vtable と混在している場合、コンパイラは構築された vtable の一部を再利用して派生クラスの vtable を作成できないためです。これにより、不要なオフセットが計算され、パフォーマンスが低下します。
このような配置により、仮想ベースは vtables に次の追加要素も導入します:vcall
オフセット (完全なオブジェクト内の仮想ベースへのポインターから仮想関数をオーバーライドするクラスの先頭にジャンプするときに、最終オーバーライダーのアドレスを取得するため)そこで定義された各仮想関数に対して。また、各仮想ベースはvbase
、派生クラスの vtable に挿入されるオフセットを追加します。仮想ベースのデータがどこから始まるかを見つけることができます (実際のアドレスは階層に依存するため、プリコンパイルできません: 仮想ベースはオブジェクトの最後にあり、最初からのシフトは非仮想ベースの数によって異なります)。現在のクラスが継承するクラス)。
うわー、不必要な複雑さをあまり導入していないことを願っています。いずれにせよ、元の標準、または独自のコンパイラの任意のドキュメントを参照できます。