为什么要另一个答案?
好吧,许多关于 SO 的帖子和外面的文章都说,钻石问题是通过创建单个实例A
而不是两个(每个父级一个D
)来解决的,从而解决了歧义。然而,这并没有让我对过程有全面的了解,我最终得到了更多的问题,比如
- 如果
B
并C
尝试创建不同的实例,A
例如调用具有不同参数的参数化构造函数(D::D(int x, int y): C(x), B(y) {}
)怎么办?A
将选择哪个实例成为其中的一部分D
?
- 如果我使用非虚拟继承
B
,而使用虚拟继承C
呢?A
创建in的单个实例是否足够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
and C
,它们每个都创建自己的A
,所以我们有 double A
in 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
to D
,但B
将创建自己的实例,A
从而将钻石问题带回
- 在孙子类而不是直接子类中定义基类参数不是一个好习惯,因此当存在菱形问题并且这种措施不可避免时应该容忍。