仮想継承は、「ダイヤモンド」(複数の継承)のあいまいさをどのように解決しますか?
-
27-09-2019 - |
質問
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
仮想継承は、ベースのインスタンスが1つしかないことを意味します A
クラスではなくクラス。
あなたのタイプ D
2つのvtableポインターがあります(最初の図で表示できます)、1つは B
そして、1つ C
事実上継承する人 A
. D
現在、2つのポインターを保存しているため、オブジェクトサイズが増加しています。ただし、1つしかありません A
今。
それで 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 -'
したがって、データの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の多くの投稿と外の記事は、ダイヤモンドの問題は単一のインスタンスを作成することによって解決されると言います A
2つの代わりに(1つの親に1つ D
)、したがって、あいまいさを解決します。しかし、これはプロセスの包括的な理解を与えてくれませんでした、私はそのようなさらに多くの質問になりました
- 仮に
B
とC
さまざまなインスタンスを作成しようとしますA
たとえば、異なるパラメーターを持つパラメーター化されたコンストラクターを呼び出す(D::D(int x, int y): C(x), B(y) {}
)?どのインスタンスA
一部になるために選ばれますD
? - 非仮想的な継承を使用した場合はどうなりますか
B
, 、しかし仮想的なものC
?単一のインスタンスを作成するのに十分ですか?A
のD
? - マイナーなパフォーマンスコストやその他の欠点がない可能性のあるダイヤモンドの問題を解決するため、予防措置としてデフォルトで仮想継承を常にデフォルトで使用する必要がありますか?
コードサンプルを試しなくても動作を予測できないことは、概念を理解しないことを意味します。以下は、仮想継承の周りに頭を包むのに役立ったものです。
ダブル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
自動的に行うつもりはありません。
この声明を念頭に置いて、私が持っていたすべての質問に答えるのは簡単です。
- その間
D
どちらも創造しませんB
またはC
のパラメーターに責任がありますA
, 、それは完全に続いていますD
それだけ。 C
の作成を委任しますA
にD
, 、 しかしB
独自のインスタンスを作成しますA
したがって、ダイヤモンドの問題を取り戻します- 直接の子供ではなく孫クラスで基本クラスのパラメーターを定義することは良い習慣ではないため、ダイヤモンドの問題が存在し、この尺度が避けられない場合に容認する必要があります。
問題はそうではありません 道 コンパイラが従う必要があります。問題はです 終点 その道の:キャストの結果。タイプ変換に関しては、パスは重要ではなく、最終結果だけが重要です。
通常の継承を使用する場合、各パスには独自の特徴的なエンドポイントがあります。つまり、キャストの結果は曖昧であり、これが問題です。
仮想継承を使用する場合、ダイヤモンド型の階層を取得します。両方のパスが同じエンドポイントにつながります。この場合、両方のパスが同じ結果につながるため、パスを選択する問題は存在しなくなります(または、より正確には問題ではありません)。結果はもはや曖昧ではありません - それが重要です。正確なパスはありません。
実際、例は次のとおりです。
#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」
仮想継承は、祖父の重複のみを解決します!ただし、メソッドを正しく過剰に取得するために、仮想である方法を指定する必要があります...