恐ろしいダイアモンドには単一のベースがあり、そこから2つの中間オブジェクトが派生し、4番目のタイプは中間レベルの両方のタイプからの多重継承でダイアモンドを閉じます。
あなたの質問は、前の例でいくつの関数が宣言されているかということのようです。f
答えは1つです。
ベースと派生の線形階層のより簡単な例から始めましょう。
struct base {
virtual void f() {}
};
struct derived : base {
virtual void f() {}
};
この例では、f
2つのオーバーライドがある宣言された単一のものがbase::f
ありますderived::f
。タイプのオブジェクトではderived
、最終的なオーバーライドはderived::f
です。f
両方の関数が、複数の実装を持つ単一の関数を表すことに注意することが重要です。
さて、元の例に戻って、右側の行でBase::f
、Right::f
同じようにオーバーライドされたのと同じ関数です。したがって、タイプがのオブジェクトのRight
場合、最終的なオーバーライドはRight::f
です。これで、タイプのfinalオブジェクトのLeft
場合、finalオーバーライドは関数をオーバーライドBase::f
しLeft
ないようになります。
ひし形が閉じているとき、および継承がvirtual
存在するため、単一のBase
オブジェクトが存在するため、単一の関数を宣言しf
ます。継承の第2レベルでは、Right
その関数を独自の実装でオーバーライドします。これは、最も派生した型の最後のオーバーライドですBottom
。
これを標準の外で見て、これが実際にコンパイラによってどのように実装されているかを確認することをお勧めします。コンパイラは、オブジェクトを作成するときに、仮想テーブルにBase
非表示のポインタを追加します。vptr
仮想テーブルはサンクへのポインターを保持します(簡単にするために、テーブルが関数の最後のオーバーライドへのポインターを保持していると仮定します[1])。この場合、Base
オブジェクトにはメンバーデータは含まれず、関数へのポインターを保持するテーブルへのポインターのみが含まれますBase::f
。
Left
extendsを拡張するBase
と、新しいvtableが作成され、そのvtable内のポインターがこのレベルLeft
の最後のオーバーライドに設定されます。これは偶然にも両方のvtable内のポインター(トランポリンを無視)が同じ実際の実装にジャンプするためです。タイプのオブジェクトが構築されているとき、サブオブジェクトが最初に初期化され、次に(存在する場合)のメンバーの初期化の前に、参照するようにポインターが更新されます(つまり、に格納されたポインターはに対して定義されたテーブルを参照します)。f
Base::f
Left
Base
Left
Base::vptr
Left::vtable
Base
Left
ひし形の反対側では、作成されたvtableに、を呼び出すことRight
になる単一のサンクRight::f
が含まれています。タイプのオブジェクトRight
が作成される場合、同じ初期化プロセスが発生し、Base::vptr
はを指しDerived::f
ます。
これで、最終オブジェクトに到達しますBottom
。この場合も、タイプに対してvtableが生成され、Bottom
そのvtableには、他のすべての場合と同様に、を表す単一のエントリが含まれますf
。コンパイラは継承の階層を分析し、Right::f
オーバーライドを決定しますBase::f
。左側のブランチには同等のオーバーライドがないため、Bottom
のvtableでは、を表すポインタはf
を参照しRight::f
ます。Bottom
この場合も、オブジェクトの構築中に、のvtableBase::vptr
を参照するようにが更新されます。Bottom
ご覧のとおり、4つのvtableすべてに1つのエントリがf
あり、各vtableに格納されている値が異なっていても(最終的なオーバーライドは異なります)、プログラムには1つのエントリがあります。 f
[1]サンクthis
は、必要に応じてポインターを適応させ(通常、多重継承が必要であることを意味します)、呼び出しを実際のオーバーライドに転送する小さなコードです。単一継承の場合、this
ポインターを更新する必要はなく、サンクは消え、vtableのエントリは実際の関数を直接指します。