为什么是另一个答案?
嗯,许多关于 SO 的帖子和外面的文章都说,钻石问题是通过创建 A 的单个实例而不是两个(D 的每个父级一个)来解决的,从而解决了歧义。然而,这并没有让我对流程有全面的了解,我最终得到了更多的问题,比如
- 如果
B 和C 尝试创建A 的不同实例会怎样?调用具有不同参数的参数化构造函数 (D::D(int x, int y): C(x), B(y) {})?将选择A 的哪个实例成为D 的一部分?
- 如果我对
B 使用非虚拟继承,而对C 使用虚拟继承呢?在D 中创建A 的单个实例是否足够?
- 从现在开始我是否应该始终默认使用虚拟继承作为预防措施,因为它解决了可能的菱形问题,性能成本低且没有其他缺点?
如果不尝试代码示例就无法预测行为意味着不理解这个概念。以下是帮助我了解虚拟继承的内容。
双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,所以我们在d 中有双重A:
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 实例,从而将钻石问题带回来
- 在孙类而不是直接子类中定义基类参数不是一个好的做法,因此当存在菱形问题并且这种措施不可避免时应该容忍。