Почему еще один ответ?
Что ж, во многих сообщениях на SO и в других статьях говорится, что проблема с бриллиантами решается путем создания одного экземпляра A
вместо двух (по одному для каждого родителя D
), тем самым устраняя двусмысленность. Однако это не дало мне полного понимания процесса, у меня возникло еще больше вопросов, например
- что, если
B
и C
пытается создать разные экземпляры, A
например, вызывая параметризованный конструктор с разными параметрами ( D::D(int x, int y): C(x), B(y) {}
)? Какой экземпляр A
будет выбран, чтобы стать его частью D
?
- что, если я использую не виртуальное наследование
B
, а виртуальное C
? Этого достаточно для создания одного экземпляра A
in 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;
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;
cout << "d.A::getX() = " << d.A::getX() << endl;
...
Перейдем к созданию 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
таким образом , принося проблемы алмазов назад
- Определение параметров базового класса в классе внука, а не в прямом дочернем, не является хорошей практикой, поэтому его следует терпеть, когда существует проблема с алмазом, и эта мера неизбежна.