_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つのパスがありますが、それ自体では決定できません。
それでは、仮想継承はどのように問題を解決しますか(コンパイラーが決定を下すのを助ける)?
あなたが望む:(仮想継承で達成可能)
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のメンバーへの呼び出しはあいまいになり、どのパスを使用するかを指定する必要があります。
派生クラスのインスタンスは、基本クラスのインスタンスを「含む」ため、メモリ内では次のようになります。
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
さて、SOおよびその他の記事では、ダイヤモンドの問題は、2つの代わりにA
の単一インスタンス(D
の親ごとに1つ)を作成することで解決します。 、このようにあいまいさを解決しました。しかし、これはプロセスの包括的な理解を与えるものではなく、
B
とC
がA
の異なるインスタンスを作成しようとした場合異なるパラメーター(D::D(int x, int y): C(x), B(y) {}
)でパラメーター化されたコンストラクターを呼び出しますか? A
のどのインスタンスがD
の一部になるために選択されますか?B
に非仮想継承を使用し、C
に仮想継承を使用するとどうなりますか? A
にD
の単一インスタンスを作成するのに十分ですか?コードサンプルを試さずに動作を予測できないということは、概念を理解していないことを意味します。以下は、仮想継承を回避するのに役立ちました。
まず、仮想継承なしでこのコードから始めましょう。
_#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
が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)
_
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
を作成することを意味することを意味します。自動的に実行します。
このステートメントを念頭に置いて、私が持っていたすべての質問に簡単に答えることができます。
D
の作成中、B
もC
もA
のパラメーターの責任を負いません。完全にD
のみです。C
はA
の作成をD
に委任しますが、B
はA
の独自のインスタンスを作成するため、ダイヤモンドの問題を取り戻します問題は、コンパイラが従わなければならないpathではありません。問題は、そのパスのエンドポイント:キャストの結果です。型変換に関しては、パスは重要ではなく、最終結果のみが重要です。
通常の継承を使用する場合、各パスには独自のエンドポイントがあります。つまり、キャストの結果があいまいであり、これが問題です。
仮想継承を使用すると、菱形の階層が得られます。両方のパスが同じエンドポイントにつながります。この場合、両方のパスが同じ結果をもたらすため、パスを選択する問題はもはや存在しません(より正確には、もはや問題ではありません)。結果はもはや曖昧ではありません-それは重要なことです。正確なパスはそうではありません。
実際の例は次のとおりです。
#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"
仮想継承は祖父の重複を解決するだけです!ただし、メソッドを正しくオーバーライドするには、メソッドを仮想に指定する必要があります...
正しいコード例はこちらです。ダイヤモンドの問題:
#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;
}