あなたの質問は興味深いですが、最初の質問としては大きすぎることを目指しているのではないかと心配しています。よろしければ、いくつかの段階に分けてお答えします :)
免責事項: 私はコンパイラの作成者ではありません。この件については確かに調査しましたが、私の言葉は慎重に受け止める必要があります。不正確な点があります。そして、私は RTTI に精通していません。また、これは標準ではないため、私が説明するのは可能性です。
1. 継承の実装方法は?
注: アラインメントの問題は省略します。これは、ブロック間にパディングが含まれる可能性があることを意味するだけです。
ここでは、仮想メソッドを省略して、以下で継承がどのように実装されるかに集中しましょう。
実のところ、継承と構成には多くの共通点があります。
struct B { int t; int u; };
struct C { B b; int v; int w; };
struct D: B { int v; int w; };
次のようになります。
B:
+-----+-----+
| t | u |
+-----+-----+
C:
+-----+-----+-----+-----+
| B | v | w |
+-----+-----+-----+-----+
D:
+-----+-----+-----+-----+
| B | v | w |
+-----+-----+-----+-----+
衝撃的ですね:) ?
ただし、これは、多重継承を理解するのが非常に簡単であることを意味します。
struct A { int r; int s; };
struct M: A, B { int v; int w; };
M:
+-----+-----+-----+-----+-----+-----+
| A | B | v | w |
+-----+-----+-----+-----+-----+-----+
これらの図を使用して、派生ポインターをベース ポインターにキャストするとどうなるかを見てみましょう。
M* pm = new M();
A* pa = pm; // points to the A subpart of M
B* pb = pm; // points to the B subpart of M
前の図を使用すると、次のようになります。
M:
+-----+-----+-----+-----+-----+-----+
| A | B | v | w |
+-----+-----+-----+-----+-----+-----+
^ ^
pm pb
pa
pb
のアドレスが のアドレスとわずかに異なるという事実はpm
、コンパイラによって自動的にポインター演算によって処理されます。
2. 仮想継承の実装方法は?
仮想継承は注意が必要です。単一のV
(仮想の) オブジェクトが他のすべてのサブオブジェクトによって共有されるようにする必要があります。簡単なダイヤモンドの継承を定義しましょう。
struct V { int t; };
struct B: virtual V { int u; };
struct C: virtual V { int v; };
struct D: B, C { int w; };
表現を省略し、オブジェクト内でとサブパーツD
の両方が同じサブオブジェクトを共有することに集中します。どうすればできますか?B
C
- クラスのサイズは一定でなければならないことに注意してください
- 設計時に、B も C も、それらが一緒に使用されるかどうかを予測できないことに注意してください。
したがって、見つかった解決策は単純です。B
へC
のポインター用のスペースのみを予約し、次のようV
にします。
- stand-alone をビルドする場合
B
、コンストラクターはヒープに を割り当てV
ます。これは自動的に処理されます。
B
の一部としてビルドする場合D
、サブパーツは、コンストラクターがポインターをの場所に渡すB
ことを期待します。D
V
C
明らかに、と同じです。
ではD
、最適化により、コンストラクターがV
オブジェクト内の右側のスペースを予約できるようになります。これは、またはD
から仮想的に継承しないため、示した図が得られます (まだ仮想メソッドはありません)。B
C
B: (and C is similar)
+-----+-----+
| V* | u |
+-----+-----+
D:
+-----+-----+-----+-----+-----+-----+
| B | C | w | A |
+-----+-----+-----+-----+-----+-----+
B
from からto へのキャストは、単純なポインター演算よりも少しトリッキーであることに注意してください。単純なポインター演算ではなくA
、ポインターをたどる必要があります。B
ただし、アップキャストという悪いケースがあります。にA
戻る方法を教えてくださいB
。
この場合、マジックは によって実行されdynamic_cast
ますが、これには何らかのサポート (つまり、情報) がどこかに格納されている必要があります。これは、いわゆるRTTI
(ランタイム型情報) です。最初にそれが a の一部であるとdynamic_cast
判断し、次に D のランタイム情報を照会して、サブオブジェクト内のどこに格納されているかを確認します。A
D
D
B
サブオブジェクトがない場合は、B
0 を返す (ポインター形式) か、bad_cast
例外をスローします (参照形式)。
3. 仮想メソッドを実装するには?
一般に、仮想メソッドは、クラスごとの v-table (つまり、関数へのポインタのテーブル) と、オブジェクトごとのこのテーブルへの v-ptr によって実装されます。これは唯一の可能な実装ではなく、他の実装の方が高速であることが実証されていますが、単純であり、オーバーヘッドが予測可能です (メモリとディスパッチ速度の両方の点で)。
仮想メソッドを使用して単純な基本クラス オブジェクトを取得すると、次のようになります。
struct B { virtual foo(); };
コンピューターの場合、メンバー メソッドなどは存在しないため、実際には次のようになります。
struct B { VTable* vptr; };
void Bfoo(B* b);
struct BVTable { RTTI* rtti; void (*foo)(B*); };
から派生する場合B
:
struct D: B { virtual foo(); virtual bar(); };
これで 2 つの仮想メソッドができました。1 つは overridesB::foo
で、もう 1 つはまったく新しいメソッドです。コンピュータ表現は次のようになります。
struct D { VTable* vptr; }; // single table, even for two methods
void Dfoo(D* d); void Dbar(D* d);
struct DVTable { RTTI* rtti; void (*foo)(D*); void (*foo)(B*); };
BVTable
とがどのように似ているかに注意してください ( を前DVTable
に置いたので) ? 重要です!foo
bar
D* d = /**/;
B* b = d; // noop, no needfor arithmetic
b->foo();
foo
呼び出しを機械語に (ある程度)翻訳してみましょう。
// 1. get the vptr
void* vptr = b; // noop, it's stored at the first byte of B
// 2. get the pointer to foo function
void (*foo)(B*) = vptr[1]; // 0 is for RTTI
// 3. apply foo
(*foo)(b);
これらの vptrs は、オブジェクトのコンストラクターによって初期化されます。 のコンストラクターを実行すると、次のようになりますD
。
D::D()
B::B()
何よりもまず、そのサブパーツを初期化するために呼び出します
B::B()
vptr
その vtable を指すように初期化してから、戻ります
D::D()
vptr
その vtable を指すように初期化し、B をオーバーライドします
したがって、vptr
ここでは D の vtable を指しているため、foo
適用されたのは D でした。それB
は完全に透明だったからです。
ここでは、B と D が同じ vptr を共有しています。
4. 多重継承の仮想テーブル
残念ながら、この共有は常に可能であるとは限りません。
まず、これまで見てきたように、仮想継承の場合、「共有」アイテムが最終的な完全なオブジェクトに奇妙に配置されます。したがって、独自の vptr があります。それは1です。
第 2 に、多重継承の場合、最初のベースは完全なオブジェクトと位置合わせされますが、2 番目のベースはそうすることができず (どちらもデータ用のスペースが必要です)、その vptr を共有できません。それは2です。
第 3 に、最初のベースは完全なオブジェクトと一致しているため、単純な継承の場合と同じレイアウトが提供されます (同じ最適化の機会)。それは3です。
とても簡単ですよね?