web-dev-qa-db-ja.com

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

_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つのパスがありますが、それ自体では決定できません。

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

77
Moeb

あなたが望む:(仮想継承で達成可能)

  A  
 / \  
B   C  
 \ /  
  D 

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

A   A  
|   |
B   C  
 \ /  
  D 

仮想継承とは、ベースAクラスのインスタンスが2つではなく1つのみであることを意味します。

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

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

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

89
Brian R. Bondy

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

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
40
el.pescado

なぜ別の答えですか?

さて、SOおよびその他の記事では、ダイヤモンドの問題は、2つの代わりにAの単一インスタンス(Dの親ごとに1つ)を作成することで解決します。 、このようにあいまいさを解決しました。しかし、これはプロセスの包括的な理解を与えるものではなく、

  1. BCAの異なるインスタンスを作成しようとした場合異なるパラメーター(D::D(int x, int y): C(x), B(y) {})でパラメーター化されたコンストラクターを呼び出しますか? AのどのインスタンスがDの一部になるために選択されますか?
  2. Bに非仮想継承を使用し、Cに仮想継承を使用するとどうなりますか? ADの単一インスタンスを作成するのに十分ですか?
  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);にはBCの両方が必要です。それぞれが独自のAを作成するため、Adが2つあります。

_Create d(2,3): 
A::A(2) C::C(2) A::A(3) B::B(3) D::D(2, 3) 
_

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

_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) 
_

ABのコンストラクターから渡されたパラメーターを無視して、デフォルトのコンストラクターで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の作成中、BCAのパラメーターの責任を負いません。完全にDのみです。
  2. CAの作成をDに委任しますが、BAの独自のインスタンスを作成するため、ダイヤモンドの問題を取り戻します
  3. 直接の子ではなく、孫クラスで基本クラスのパラメーターを定義することは良い習慣ではないため、ダイヤモンドの問題が存在し、この測定が避けられない場合は許容する必要があります。
17
nnovich-OK

問題は、コンパイラが従わなければならないpathではありません。問題は、そのパスのエンドポイント:キャストの結果です。型変換に関しては、パスは重要ではなく、最終結果のみが重要です。

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

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

9
AnT

実際の例は次のとおりです。

#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"

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

8
enger

正しいコード例はこちらです。ダイヤモンドの問題:

#include <iostream>
// Here you have the diamond problem : there is B::eat() and C::eat()
// because they both inherit from A and contain independent copies of A::eat()
// So what is D::eat()? Is it B::eat() or C::eat() ?
class A { public: void eat(){ std::cout << "CHROME-CHROME" << endl; } };
class B: public A   { };
class C: public A   { };
class D: public B,C { };

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

ソリューション :

#include <iostream>
// Virtual inheritance to ensure B::eat() and C::eat() to be the same 
class A { public: void eat(){ std::cout<< "CHROME-CHROME" << endl; } };
class B: virtual public A   { };
class C: virtual public A   { };
class D: public         B,C { };

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