clas(child)がbase1とbase2を複数継承する基本クラスを持つ単一の継承を持つオブジェクトに通常必要なvptrの数。オブジェクトが単一の継承と多重継承のカップルを持っている場合に、オブジェクトが提供したvptrの数を識別するための戦略は何ですか。標準ではvptrsについて指定されていませんが、実装が仮想関数の実装をどのように行うかを知りたいだけです。
2 に答える
なんで気にするの?簡単な答えで十分ですが、もっと完全なものが必要だと思います。
これは標準の一部ではないため、実装は自由に行うことができますが、一般的な経験則では、仮想テーブル ポインターを使用する実装では、ゼロ次近似として、最大で必要な動的ディスパッチに次のようになります。階層に新しい仮想メソッドを追加するクラスがあるため、仮想テーブルへの多くのポインター。(場合によっては、仮想テーブルを拡張でき、基本型と派生型が単一の を共有しますvptr
)
// some examples:
struct a { void foo(); }; // no need for virtual table
struct b : a { virtual foo1(); }; // need vtable, and vptr
struct c : b { void bar(); }; // no extra virtual table, 1 vptr (b) suffices
struct d : b { virtual bar(); }; // 1 vtable, d extends b's vtable
struct e : d, b {}; // 2 vptr, 1 for the d and 1 for b
struct f : virtual b {}; // 1 vptr, f reuse b's vptr to locate subobject b
struct g : virtual b {}; // 1 vptr, g reuse b's vptr to locate subobject b
struct h : f, g {}; // 2 vptr, 1 for f, 1 for g
// h can locate subobject b using f's vptr
基本的に、独自の動的ディスパッチを必要とする (親を直接再利用できない) タイプの各サブオブジェクトには、独自の仮想テーブルと vptr が必要です。
実際には、コンパイラはさまざまな vtable を 1 つの vtable にマージします。d
の一連の関数に新しい仮想関数を追加するとb
、コンパイラは vtable の末尾に新しいスロットを追加することで、潜在的な 2 つのテーブルを 1 つのテーブルにマージします。そのため、vtable ford
は vtable for の拡張バージョンになります。b
最後に追加の要素を追加してバイナリ互換性を維持し (つまり、d
vtable を vtable として解釈して でb
使用可能なメソッドにアクセスできますb
)、d
オブジェクトには単一のvptr
.
多重継承の場合、各ベースが完全なオブジェクトのサブオブジェクトと同じレイアウトを持つ必要があるため、それが個別のオブジェクトである場合よりも少し複雑になります。 vtable.
最後に、仮想継承の場合、事態はさらに複雑になり、同じ完全なオブジェクトに対して複数の vtable が存在し、vptr が構築/破壊の進化に応じて更新される可能性があります (vptr は構築/破壊の進化に応じて常に更新されますが、仮想継承がなければ、 vptr はベースの vtable を指しますが、仮想継承の場合、同じタイプの vtable が複数存在します)。
細字
vptr/vtable に関しては何も指定されていないため、これは詳細についてはコンパイラに依存することになりますが、単純なケースはほとんどすべての最新のコンパイラで同じように処理されます (念のため「ほぼ」と書きます)。
あなたは警告されました。
オブジェクト レイアウト: 非仮想継承
基本クラスから継承し、それらが vptr を持っている場合、クラスには継承された vptr と同じ数の vptr が自然に存在します。
問題は、vptr が既に継承されているクラスにコンパイラがいつ vptr を追加するかということです。
コンパイラは、冗長な vptr の追加を回避しようとします。
struct B {
virtual ~B();
};
struct D : B {
virtual void foo();
};
ここB
には vptr があるためD
、独自の vptr を取得せず、既存の vptr を再利用します。の vtable はB
、 のエントリで拡張されfoo()
ます。の vtableは、擬似コードD
の vtable for から「派生」します。B
struct B_vtable {
typeinfo *info; // for typeid, dynamic_cast
void (*destructor)(B*);
};
struct D_vtable : B_vtable {
void (*foo)(D*);
};
繰り返しになりますが、これは実際の vtable を単純化して、アイデアを得たものです。
仮想継承
非仮想単一継承の場合、実装間のバリエーションの余地はほとんどありません。仮想継承については、コンパイラ間にさらに多くのバリエーションがあります。
struct B2 : virtual A {
};
B2*
からへの変換があるA*
ため、B2
オブジェクトは次の機能を提供する必要があります。
A*
メンバーと一緒か- int メンバーのいずれか:
offset_of_A_from_B2
- vptr を使用するか
offset_of_A_from_B2
、vtableに格納する
一般に、クラスはその仮想基底クラスの vptr を再利用しません(ただし、非常に特殊なケースでは再利用できます)。