13

これは、C++11 標準の sec 12.7.4 からのものです。これはかなり紛らわしいです。

  1. テキストの最後の文は正確には何を意味していますか?
  2. 最後のメソッド呼び出しがB::B未定義なのはなぜですか? 呼ぶだけじゃないのa.A::f

4 仮想関数 (10.3) を含むメンバー関数は、構築または破棄 (12.6.2) 中に呼び出すことができます。クラスの非静的データ メンバーの構築中または破棄中を含め、コンストラクタまたはデストラクタから仮想関数が直接的または間接的に呼び出され、呼び出しが適用されるオブジェクトが構築中のオブジェクト (x と呼ぶ) である場合または破壊の場合、呼び出される関数は、コンストラクタまたはデストラクタのクラスの最終オーバーライドであり、より派生したクラスでそれをオーバーライドするものではありません。仮想関数呼び出しが明示的なクラス メンバー アクセス (5.2.5) を使用し、オブジェクト式が x またはその基本クラス サブオブジェクトの 1 つではなく、x またはその基本クラス サブオブジェクトの 1 つの完全なオブジェクトを参照する場合、動作は未定義です。 . [ 例:

struct V {
 virtual void f();
 virtual void g();
};

struct A : virtual V {
 virtual void f();
};

struct B : virtual V {
 virtual void g();
 B(V*, A*);
};

struct D : A, B {
 virtual void f();
 virtual void g();
 D() : B((A*)this, this) { }
};

B::B(V* v, A* a) {
 f(); // calls V::f, not A::f
 g(); // calls B::g, not D::g
 v->g(); // v is base of B, the call is well-defined, calls B::g
 a->f(); // undefined behavior, a’s type not a base of B
}

—終わりの例]

4

3 に答える 3

19

標準のその部分は、J基本クラス階層に多重継承が含まれる「大きな」オブジェクトを構築していて、現在、基本サブオブジェクトのコンストラクター内に座っている場合、およびHのポリモーフィズムのみを使用できることを単に伝えています。Hその直接および間接の基本サブオブジェクト。その下位階層の外でポリモーフィズムを使用することは許可されていません。

たとえば、次の継承図を考えてみましょう (矢印は派生クラスから基本クラスを指しています)。

ここに画像の説明を入力

type の「大きな」オブジェクトを構築しているとしましょうJ。そして、現在 class のコンストラクターを実行していHます。youのコンストラクHター内では、赤い楕円内の下位階層の典型的なコンストラクター制限ポリモーフィズムを楽しむことができます。たとえば、タイプ のベース サブオブジェクトの仮想関数を呼び出すことができB、多態的な動作は円で囲まれたサブ階層内で期待どおりに機能します (「期待どおり」とは、多態的な動作がH階層内と同じくらい低くなるが、それより低くなることはないことを意味します)。AEXおよび赤い楕円の内側にあるその他のサブオブジェクトの仮想関数を呼び出すこともできます。

ただし、何らかの方法で楕円の外側の階層にアクセスし、そこでポリモーフィズムを使用しようとすると、動作が未定義になります。Gたとえば、何らかの方法で のコンストラクターからサブオブジェクトにアクセスし、 - のH仮想関数を呼び出そうとした場合G、動作は未定義です。のコンストラクターからのD仮想関数の呼び出しについても同じことが言えます。IH

「外部」サブ階層へのそのようなアクセスを取得する唯一の方法は、誰かが何らかの方法で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動作は未定義です。

これは、実際の実験で確認するのはかなり簡単です。の独自のバージョンを追加して、これfB行いましょう

#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の間で共有されているため、正確に発生します。仮想継承がなければ、動作がより予測可能になる可能性は十分にあります。ただし、言語仕様では、私の図に描かれているように線を引くことにしたようです。構築中は、どの継承タイプが使用されているかに関係なく、サブ階層の「サンドボックス」から抜け出すことはできません。ABHH

于 2012-07-07T19:29:02.103 に答える
1

あなたが引用した規範的なテキストの最後の文は次のようになっています。

仮想関数呼び出しが明示的なクラス メンバー アクセスを使用し、オブジェクト式が完全なオブジェクトxまたはそのオブジェクトの基底クラス サブオブジェクトの 1 つを参照しているが、xまたはその基底クラス サブオブジェクトの 1 つを参照していない場合、動作は未定義です。

確かに、これはかなり複雑です。この文は、多重継承が存在する場合に構築中に呼び出される可能性がある関数を制限するために存在します。

この例には複数の継承が含まれています: andDから派生しています (動作が定義されていない理由を示す必要がないため、は無視します)。オブジェクトの構築中に、コンストラクターとコンストラクターの両方が呼び出されて、オブジェクトの基本クラス サブオブジェクトが構築されます。ABVDABD

Bコンストラクターが呼び出されると、の完全なオブジェクトのx型は ですD。そのコンストラクタでは、 の基本クラス サブオブジェクトaへのポインタです。したがって、 について次のことが言えます。Axa->f()

  • 構築中のオブジェクトはB、オブジェクトの基本クラスのサブDオブジェクトです (この基本クラスのサブオブジェクトは現在構築中のオブジェクトであるため、テキストでは と呼ばれていますx)。

  • 明示的なクラス メンバー アクセスを使用します (->この場合、演算子を介して)

  • の完全なオブジェクトのx型はです。これはD、構築されている最も派生した型であるためです。

  • オブジェクト式( a) は、完全なオブジェクトの基本xクラス サブオブジェクトを参照します (構築されるAオブジェクトの基本クラス サブオブジェクトを参照します)。D

  • オブジェクト式が参照する基本クラスxサブオブジェクトは、 の基本クラス サブオブジェクトでxAなくBAの基本クラスでもありませんB

したがって、呼び出しの動作は、最初に開始したルールに従って未定義です。

最後のメソッド呼び出しがB::B未定義なのはなぜですか? 呼び出すだけではいけませんa.A::fか?

あなたが引用したルールは、構築中にコンストラクターが呼び出された場合、「呼び出された関数はコンストラクターのクラスの最終オーバーライドであり、より派生したクラスでそれをオーバーライドする関数ではない」と述べています。

この場合、コンストラクタのクラスはB. Bは から派生していないためA、仮想関数の最終オーバーライドはありません。したがって、仮想呼び出しを実行しようとすると、未定義の動作が発生します。

于 2012-07-07T18:51:33.890 に答える
0

私がこれをどのように理解しているかは次のとおりです。オブジェクトの構築中に、各サブオブジェクトがその部分を構築します。この例では、のメンバーをV::V()初期化することを意味します。のメンバーなどを初期化します。は と の前に初期化されるため、どちらものメンバーに依存して初期化できます。VAAVABV

この例では、Bのコンストラクターは、それ自体への 2 つのポインターを受け入れます。そのV部分はすでに構築されているので、安全に呼び出すことができますv->g()。ただし、その時点ではDA部分はまだ初期化されていません。したがって、呼び出しa->f()は未定義の動作である初期化されていないメモリにアクセスします。

編集:

D上記では、 は のA前に初期化されるため、 の初期化されていないメモリBへのアクセスはありません。A一方、Aが完全に構築されると、その仮想関数は の仮想関数によってオーバーライドされますD(実際には、その vtable はA構築中に に設定されD、構築が終了すると に設定されます)。したがって、へのa->f()呼び出しはD::f()、初期化される前に呼び出さDれます。したがって、どちらの方法でも -A前または後に構築されBます - 初期化されていないオブジェクトでメソッドを呼び出すことになります。

仮想関数の部分については既にここで説明しましたが、完全を期すために: への呼び出しは、まだ初期化されていないため、f()使用します。をオーバーライドするため、呼び出します。V::fABfg()B::gBg

于 2012-07-07T18:46:57.433 に答える