標準のその部分は、J
基本クラス階層に多重継承が含まれる「大きな」オブジェクトを構築していて、現在、基本サブオブジェクトのコンストラクター内に座っている場合、およびH
のポリモーフィズムのみを使用できることを単に伝えています。H
その直接および間接の基本サブオブジェクト。その下位階層の外でポリモーフィズムを使用することは許可されていません。
たとえば、次の継承図を考えてみましょう (矢印は派生クラスから基本クラスを指しています)。
type の「大きな」オブジェクトを構築しているとしましょうJ
。そして、現在 class のコンストラクターを実行していH
ます。youのコンストラクH
ター内では、赤い楕円内の下位階層の典型的なコンストラクター制限ポリモーフィズムを楽しむことができます。たとえば、タイプ のベース サブオブジェクトの仮想関数を呼び出すことができB
、多態的な動作は円で囲まれたサブ階層内で期待どおりに機能します (「期待どおり」とは、多態的な動作がH
階層内と同じくらい低くなるが、それより低くなることはないことを意味します)。A
、E
、X
および赤い楕円の内側にあるその他のサブオブジェクトの仮想関数を呼び出すこともできます。
ただし、何らかの方法で楕円の外側の階層にアクセスし、そこでポリモーフィズムを使用しようとすると、動作が未定義になります。G
たとえば、何らかの方法で のコンストラクターからサブオブジェクトにアクセスし、 - のH
仮想関数を呼び出そうとした場合G
、動作は未定義です。のコンストラクターからのD
仮想関数の呼び出しについても同じことが言えます。I
H
「外部」サブ階層へのそのようなアクセスを取得する唯一の方法は、誰かが何らかの方法でG
サブオブジェクトへのポインタ/参照を のコンストラクタに渡した場合ですH
。したがって、標準テキストでの「明示的なクラス メンバー アクセス」への参照 (ただし、過剰に思われます)。
標準では、この規則がどれほど包括的であるかを示すために、例に仮想継承が含まれています。上の図では、ベース サブオブジェクトX
は、楕円形の内側のサブ階層と楕円形の外側のサブ階層の両方で共有されています。X
標準では、 のコンストラクターからサブオブジェクトの仮想関数を呼び出しても問題ないと規定されていH
ます。
この制限はD
、 、G
およびI
サブオブジェクトの構築が の構築がH
開始される前に終了した場合でも適用されることに注意してください。
この仕様のルーツは、多態性メカニズムの実装に関する実際的な考察につながります。実際の実装では、VMT ポインターはデータ フィールドとして、階層内の最も基本的なポリモーフィック クラスのオブジェクト レイアウトに導入されます。派生クラスは、独自の VMT ポインターを導入しません。基本クラス (および、場合によっては、より長い VMT) によって導入されたポインターに独自の特定の値を提供するだけです。
標準の例を見てみましょう。クラスA
は class から派生しV
ます。これは、 の VMT ポインターが物理的にサブオブジェクトA
に属していることを意味します。V
によって導入された仮想関数へのすべての呼び出しは、によってV
導入された VMT ポインターを介してディスパッチされV
ます。つまり、あなたが電話するたびに
pointer_to_A->f();
それは実際に翻訳されます
V *v_subobject = (V *) pointer_to_A; // go to V
vmt = v_subobject->vmt_ptr; // retrieve the table
vmt[index_for_f](); // call through the table
ただし、標準の例では、まったく同じV
サブオブジェクトが にも埋め込まれてB
います。コンストラクター制限ポリモーフィズムを正しく機能させるために、コンパイラーはB
の VMT へのポインターを、 に格納されている VMT ポインターに配置しますV
(B
のコンストラクターがアクティブである間、V
サブオブジェクトは の一部として機能する必要があるためB
)。
この時点で何らかの方法で電話をかけようとすると
a->f(); // as in the example
上記のアルゴリズムはB
、そのサブオブジェクトに格納されている の VMT ポインターを見つけ、そのVMT を介してV
呼び出しを試みます。f()
これは明らかにまったく意味がありません。つまり、 の VMT をA
介してディスパッチされる仮想メソッドを持つことは意味がありません。B
動作は未定義です。
これは、実際の実験で確認するのはかなり簡単です。の独自のバージョンを追加して、これf
をB
行いましょう
#include <iostream>
struct V {
virtual void f() { std::cout << "V" << std::endl; }
};
struct A : virtual V {
virtual void f() { std::cout << "A" << std::endl; }
};
struct B : virtual V {
virtual void f() { std::cout << "B" << std::endl; }
B(V*, A*);
};
struct D : A, B {
virtual void f() {}
D() : B((A*)this, this) { }
};
B::B(V* v, A* a) {
a->f(); // What `f()` is called here???
}
int main() {
D d;
}
ここA::f
に呼ばれるつもり?私はいくつかのコンパイラを試しましたが、それらはすべて実際にB::f
! 一方、そのような呼び出しで受け取るthis
ポインター値B::f
は完全に偽物です。
http://ideone.com/Ua332
これは、まさに上で説明した理由により発生します (ほとんどのコンパイラは、上で説明した方法でポリモーフィズムを実装しています)。これが、言語がそのような呼び出しを未定義と記述する理由です。
この特定の例では、実際にはこの異常な動作につながるのは仮想継承であることに気付くかもしれません。はい、サブオブジェクトがとサブオブジェクトV
の間で共有されているため、正確に発生します。仮想継承がなければ、動作がより予測可能になる可能性は十分にあります。ただし、言語仕様では、私の図に描かれているように線を引くことにしたようです。構築中は、どの継承タイプが使用されているかに関係なく、サブ階層の「サンドボックス」から抜け出すことはできません。A
B
H
H