Чому інша відповідь?
Що ж, багато публікацій про SO та статті поза межами кажуть, що алмазна проблема вирішується шляхом створення одного примірника A
замість двох (по одному для кожного з батьків D
), тим самим вирішуючи двозначність. Однак це не дало мені всебічного розуміння процесу, у мене з’явилося ще більше запитань
- що робити, якщо
B
і C
намагається створити різні екземпляри, A
наприклад, виклик параметризованого конструктора з різними параметрами ( D::D(int x, int y): C(x), B(y) {}
)? Який екземпляр A
буде обраний, щоб стати частиною D
?
- що робити, якщо я використовую невіртуальну спадщину для
B
, а віртуальну для C
? Чи достатньо для створення одного примірника A
в 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;
// 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
, таким чином , приносячи проблеми алмазів назад
- Визначення параметрів базового класу в класі онука, а не прямої дитини не є хорошою практикою, тому його слід допускати, коли існує проблема з алмазами, і цей захід неминучий.