Почему другой ответ?
Что ж, во многих публикациях по SO и статьям вне говорится, что проблема с алмазом решается путем создания одного экземпляра A
вместо двух (по одному для каждого родителя 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
, тем самым возвращая проблему с бриллиантами
- Определение параметров базового класса в классе внука, а не в прямом дочернем процессе, не является хорошей практикой, поэтому его следует терпеть, когда существует проблема с алмазом, и эта мера неизбежна.