Чому інша відповідь?
Що ж, багато публікацій про 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 , таким чином , приносячи проблеми алмазів назад
- Визначення параметрів базового класу в класі онука, а не прямої дитини не є хорошою практикою, тому його слід допускати, коли існує проблема з алмазами, і цей захід неминучий.