仮想継承はどのようにして「ダイヤモンド」(多重継承)のあいまいさを解決しますか?


95
class A                     { public: void eat(){ cout<<"A";} }; 
class B: virtual public A   { public: void eat(){ cout<<"B";} }; 
class C: virtual public A   { public: void eat(){ cout<<"C";} }; 
class D: public         B,C { public: void eat(){ cout<<"D";} }; 

int main(){ 
    A *a = new D(); 
    a->eat(); 
} 

ダイヤモンドの問題を理解しています。上記のコードにはその問題はありません。

仮想継承はどの程度正確に問題を解決しますか?

私が理解すること: 私が言うときA *a = new D();、コンパイラーは、型のオブジェクトを型Dのポインターに割り当てることができるかどうかを知りたいのですAが、それには2つのパスがありますが、それ自体では決定できません。

それで、仮想継承はどのように問題を解決しますか(コンパイラが決定を下すのを助けます)?

回答:


109

必要なもの:(仮想継承で実現可能)

  A  
 / \  
B   C  
 \ /  
  D 

そしてない:(仮想継承なしで何が起こるか)

A   A  
|   |
B   C  
 \ /  
  D 

仮想継承とは、基本Aクラスのインスタンスが2 つではなく1つしかないことを意味します。

タイプDには2つのvtableポインター(最初の図で確認できます)があり、1 BつはCを継承し、もう1つはを継承していAます。 Dのオブジェクトサイズは、現在2つのポインタを格納しているため、増加しています。ただし、現在は1つだけAです。

だから、B::AC::A同じであり、したがってからのあいまいな呼び出しはあり得ませんD。仮想継承を使用しない場合は、上記の2番目の図になります。そして、Aのメンバーへの呼び出しはあいまいになり、どのパスを使用するかを指定する必要があります。

ウィキペディアには、ここで別の良い要約と例があります


2
Vtableポインタは実装の詳細です。この場合、すべてのコンパイラがvtableポインタを導入するわけではありません。
curiousguy

19
グラフを垂直にミラーリングすると、見栄えが良くなると思います。ほとんどの場合、私はそのような継承図を見つけて、ベースの下の派生クラスを示しています。(「ダウンキャスト」、「アップキャスト」を参照)
peterh-モニカを2016年

代わりにBCの実装を使用するように彼のコードを変更するにはどうすればよいですか?ありがとう!
MinhNghĩa19年

44

派生クラスのインスタンスは、基本クラスのインスタンスを「含む」ため、メモリ内では次のように見えます。

class A: [A fields]
class B: [A fields | B fields]
class C: [A fields | C fields]

したがって、仮想継承がない場合、クラスDのインスタンスは次のようになります。

class D: [A fields | B fields | A fields | C fields | D fields]
          '- derived from B -' '- derived from C -'

したがって、Aデータの2つの「コピー」に注意してください。仮想継承とは、派生クラス内に、実行時に基本クラスのデータを指すvtableポインターセットがあるため、B、C、およびDクラスのインスタンスが次のようになることを意味します。

class B: [A fields | B fields]
          ^---------- pointer to A

class C: [A fields | C fields]
          ^---------- pointer to A

class D: [A fields | B fields | C fields | D fields]
          ^---------- pointer to B::A
          ^--------------------- pointer to C::A


43

なぜ別の答えですか?

さて、SOに関する多くの投稿と外部の記事によると、ダイヤモンドの問題はA2つではなく1つのインスタンス(の各親に対して1つD)を作成することで解決され、あいまいさを解決しています。しかし、これではプロセスを包括的に理解できなかったため、次のような質問がさらに多くなりました。

  1. たとえば、異なるパラメーター()でパラメーター化されたコンストラクターを呼び出すなどの異なるインスタンスを作成しようとするとどうBなりますか?の一部になるために選択されるのはどのインスタンスですか?CAD::D(int x, int y): C(x), B(y) {}AD
  2. に非仮想継承を使用しB、仮想継承を使用するとCどうなりますか?Ainの単一インスタンスを作成するのに十分Dですか?
  3. 小さなダイヤモンドのパフォーマンスコストで他の欠点はなく、可能性のあるダイヤモンドの問題を解決するので、予防策としてこれからデフォルトで常に仮想継承を使用する必要がありますか?

コードサンプルを試さずに動作を予測できないことは、概念を理解していないことを意味します。以下は、仮想継承に頭を悩ますのに役立つものです。

ダブルA

まず、仮想継承なしのこのコードから始めましょう:

#include<iostream>
using namespace std;
class A {
public:
    A()                { cout << "A::A() "; }
    A(int x) : m_x(x)  { cout << "A::A(" << x << ") "; }
    int getX() const   { return m_x; }
private:
    int m_x = 42;
};

class B : public A {
public:
    B(int x):A(x)   { cout << "B::B(" << x << ") "; }
};

class C : public A {
public:
    C(int x):A(x) { cout << "C::C(" << x << ") "; }
};

class D : public C, public B  {
public:
    D(int x, int y): C(x), B(y)   {
        cout << "D::D(" << x << ", " << y << ") "; }
};

int main()  {
    cout << "Create b(2): " << endl;
    B b(2); cout << endl << endl;

    cout << "Create c(3): " << endl;
    C c(3); cout << endl << endl;

    cout << "Create d(2,3): " << endl;
    D d(2, 3); cout << endl << endl;

    // error: request for member 'getX' is ambiguous
    //cout << "d.getX() = " << d.getX() << endl;

    // error: 'A' is an ambiguous base of 'D'
    //cout << "d.A::getX() = " << d.A::getX() << endl;

    cout << "d.B::getX() = " << d.B::getX() << endl;
    cout << "d.C::getX() = " << d.C::getX() << endl;
}

出力を見てみましょう。実行B b(2);するとA(2)、期待どおりに作成されますC c(3);

Create b(2): 
A::A(2) B::B(2) 

Create c(3): 
A::A(3) C::C(3) 

D d(2, 3);との両方が必要でBありC、それぞれが独自のを作成しているAので、ダブルAインしていdます:

Create d(2,3): 
A::A(2) C::C(2) A::A(3) B::B(3) D::D(2, 3) 

d.getX()コンパイラはAメソッドを呼び出すインスタンスを選択できないため、これがコンパイルエラーを引き起こす理由です。それでも、選択した親クラスのメソッドを直接呼び出すことは可能です。

d.B::getX() = 3
d.C::getX() = 2

仮想性

次に、仮想継承を追加します。次の変更を加えた同じコードサンプルを使用します。

class B : virtual public A
...
class C : virtual public A
...
cout << "d.getX() = " << d.getX() << endl; //uncommented
cout << "d.A::getX() = " << d.A::getX() << endl; //uncommented
...

の作成にジャンプしましょうd

Create d(2,3): 
A::A() C::C(2) B::B(3) D::D(2, 3) 

ご覧のAとおり、Bおよびのコンストラクターから渡されたパラメーターを無視して、デフォルトのコンストラクターで作成されていますC。あいまいさがなくなったので、すべての呼び出しがgetX()同じ値を返します。

d.getX() = 42
d.A::getX() = 42
d.B::getX() = 42
d.C::getX() = 42

しかし、パラメータ化されたコンストラクタを呼び出したい場合はどうAでしょうか?これは、次のコンストラクタから明示的に呼び出すことで実行できますD

D(int x, int y, int z): A(x), C(y), B(z)

通常、クラスは直接の親のコンストラクターのみを明示的に使用できますが、仮想継承の場合は除外されます。このルールを発見すると、「クリック」され、仮想インターフェースの理解に大いに役立ちました。

コードとclass B: virtual Aは、から継承されたすべてのクラスが自動的Bに作成AするBわけではないため、それ自体で作成する必要があることを意味します。

このステートメントを念頭に置くと、私が持っていたすべての質問に簡単に答えることができます。

  1. D作成中、BまたはのCパラメータの責任はありませんがA、完全に最大Dです。
  2. C作成委譲しますAにしDますが、B独自のインスタンスが作成されますAので、ダイヤモンドの問題のバックを持参します
  3. 直接の子ではなく孫クラスで基本クラスのパラメーターを定義することは良い習慣ではないので、ひし形の問題が存在し、この対策が避けられない場合は許容する必要があります。

10

問題は、コンパイラがたどる必要のある経路ではありません。問題はそのパスの終点、つまりキャストの結果です。型変換に関しては、パスは重要ではなく、最終的な結果だけが重要です。

通常の継承を使用する場合、各パスには独自のエンドポイントがあります。つまり、キャストの結果があいまいであり、これが問題です。

仮想継承を使用すると、菱形の階層になります。両方のパスが同じエンドポイントにつながります。この場合、両方のパスが同じ結果につながるため、パスを選択する問題はなくなりました(正確には、問題ではなくなりました)。結果はもはや曖昧ではありません-それが重要です。正確なパスはそうではありません。


@Andrey:コンパイラはどのように継承を実装するのか...つまり、私はあなたの引数を得て、それをわかりやすく説明してくれたことに感謝したいと思います。コンパイラが実際に継承を実装する方法と、仮想継承を行うと何が変わるか
Bruce

8

実際の例は次のようになります。

#include <iostream>

//THE DIAMOND PROBLEM SOLVED!!!
class A                     { public: virtual ~A(){ } virtual void eat(){ std::cout<<"EAT=>A";} }; 
class B: virtual public A   { public: virtual ~B(){ } virtual void eat(){ std::cout<<"EAT=>B";} }; 
class C: virtual public A   { public: virtual ~C(){ } virtual void eat(){ std::cout<<"EAT=>C";} }; 
class D: public         B,C { public: virtual ~D(){ } virtual void eat(){ std::cout<<"EAT=>D";} }; 

int main(int argc, char ** argv){
    A *a = new D(); 
    a->eat(); 
    delete a;
}

...そのようにして、出力は正しいものになります: "EAT => D"

仮想継承は祖父の重複を解決するだけです!しかし、メソッドを正しくオーバーライドするには、メソッドを仮想に指定する必要があります...

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.