クラスのポリモーフィックな単一継承階層がある場合、ほとんどの (すべてではないにしても) コンパイラが従う典型的な規則は、その階層内の各オブジェクトが VMT ポインター (仮想メソッド テーブルへのポインター) で始まる必要があるというものです。このような場合、VMT ポインターは早い段階でオブジェクト メモリ レイアウトに導入されます。つまり、ポリモーフィック階層のルート クラスによって、すべての下位クラスは単にそれを継承し、適切な VMT を指すように設定します。このような場合、派生オブジェクト内のすべてのネストされたサブオブジェクトは同じthis
値を持ちます。そのようにして、コンパイラでメモリ位置を読み取ること*this
により、実際のサブオブジェクトのタイプに関係なく、VMT ポインターにすぐにアクセスできます。これはまさに、最後の実験で起こったことです。ルート クラスをポリモーフィックにすると、すべてのthis
値が一致します。
ただし、階層内の基本クラスがポリモーフィックでない場合、VMT ポインターは導入されません。VMT ポインターは、階層の下位にある最初のポリモーフィック クラスによって導入されます。このような場合、一般的な実装アプローチは、階層の非ポリモーフィック (上位) 部分によって導入されたデータの前に VMT ポインターを挿入することです。これは、2 番目の実験で見られるものです。のメモリ レイアウトはDerived
次のようになります。
+------------------------------------+ <---- `this` value for `Derived` and below
| VMT pointer introduced by Derived |
+------------------------------------+ <---- `this` value for `Base` and above
| Base data |
+------------------------------------+
| Derived data |
+------------------------------------+
一方、階層の非ポリモーフィック (上位) 部分のすべてのクラスは、VMT ポインターについて何も認識しない必要があります。タイプのオブジェクトはBase
data field で始まる必要がありますBase::x
。同時に、階層のポリモーフィック (下位) 部分のすべてのクラスは、VMT ポインターで開始する必要があります。これらの両方の要件を満たすために、ネストされた基本サブオブジェクトから別のサブオブジェクトに階層を上下に変換するときに、コンパイラはオブジェクト ポインター値を調整する必要があります。これは、ポリモーフィック/非ポリモーフィック境界を越えたポインター変換がもはや概念的ではないことをすぐに意味します。コンパイラーはオフセットを追加または減算する必要があります。
階層の非ポリモーフィック部分のthis
サブオブジェクトは値を共有しますが、階層のポリモーフィック部分のサブオブジェクトは独自の異なるthis
値を共有します。
階層に沿ってポインター値を変換するときにオフセットを加算または減算する必要があるのは珍しいことではありません。複数の継承階層を処理するとき、コンパイラーは常にそれを行う必要があります。ただし、例では、単一継承階層でもそれを実現する方法を示しています。
加算/減算の効果はポインタ変換でも明らかになります
Derived *pd = new Derived;
Base *pb = pd;
// Numerical values of `pb` and `pd` are different if `Base` is non-polymorphic
// and `Derived` is polymorphic
Derived *pd2 = static_cast<Derived *>(pb);
// Numerical values of `pd` and `pd2` are the same