class A { public: void eat(){ cout<<"A";} };
class B: virtual public A { public: void eat(){ cout<<"B";} };
class C: virtual public A { public: void eat(){ cout<<"C";} };
class D: public B,C { public: void eat(){ cout<<"D";} };
int main(){
A *a = new D();
a->eat();
}
Я понимаю проблему с алмазом, и в приведенном выше фрагменте кода этой проблемы нет.
Как именно виртуальное наследование решает проблему?
Что я понимаю:
когда я говорю A *a = new D();
, компилятор хочет знать, может ли объект типа D
быть назначен указателю типа A
, но у него есть два пути, по которым он может следовать, но не может решить сам.
Итак, как виртуальное наследование решает проблему (помогите компилятору принять решение)?
B
'orC
'? Благодарность!Почему еще один ответ?
Что ж, во многих сообщениях на SO и в других статьях говорится, что проблема с бриллиантами решается путем создания одного экземпляра
A
вместо двух (по одному для каждого родителяD
), тем самым устраняя двусмысленность. Однако это не дало мне полного понимания процесса, у меня возникло еще больше вопросов, напримерB
иC
пытается создать разные экземпляры,A
например, вызывая параметризованный конструктор с разными параметрами (D::D(int x, int y): C(x), B(y) {}
)? Какой экземплярA
будет выбран, чтобы стать его частьюD
?B
, а виртуальноеC
? Этого достаточно для создания одного экземпляраA
inD
?Неспособность предсказать поведение, не попробовав образцы кода, означает непонимание концепции. Ниже описано, что помогло мне разобраться в виртуальном наследовании.
Двойной А
Во-первых, давайте начнем с этого кода без виртуального наследования:
#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
таким образом , принося проблемы алмазов назадисточник
virtual
ключевого слова как «определено позже (в подклассах)», то есть не «реально» определено, а «определено виртуально». Эта интерпретация работает не только для базовых классов, но и для методов. Спасибо!Экземпляры производных классов хранят члены своих базовых классов.
Без виртуального наследования схемы памяти выглядят так (обратите внимание на две копии
A
членов в классеD
):class A: [A members] class B: public A [A members|B members] class C: public A [A members|C members] class D: public B, public C [A members|B members|A members|C members|D members]
При виртуальном наследовании схемы памяти выглядят следующим образом (обратите внимание на единственную копию
A
членов в классеD
):class A: [A members] class B: virtual public A [B members|A members] | ^ v | virtual table B class C: virtual public A [C members|A members] | ^ v | virtual table C class D: public B, public C [B members|C members|D members|A members] | | ^ v v | virtual table D ----------------|
Для каждого производного класса компилятор создает виртуальную таблицу, содержащую указатели на члены его виртуальных базовых классов, хранящихся в производном классе, и добавляет указатель на эту виртуальную таблицу в производном классе.
источник
Проблема не в пути, по которому должен идти компилятор. Проблема в конечной точке этого пути: в результате приведения. Когда дело доходит до преобразования типов, путь не имеет значения, имеет значение только конечный результат.
Если вы используете обычное наследование, каждый путь имеет свою собственную отличительную конечную точку, что означает, что результат приведения неоднозначен, что является проблемой.
Если вы используете виртуальное наследование, вы получаете ромбовидную иерархию: оба пути ведут к одной и той же конечной точке. В этом случае проблема выбора пути больше не существует (или, точнее, перестает иметь значение), потому что оба пути приводят к одному и тому же результату. Результат больше не однозначный - вот что важно. Точный путь - нет.
источник
Собственно пример должен быть таким:
#include <iostream> //THE DIAMOND PROBLEM SOLVED!!! class A { public: virtual ~A(){ } virtual void eat(){ std::cout<<"EAT=>A";} }; class B: virtual public A { public: virtual ~B(){ } virtual void eat(){ std::cout<<"EAT=>B";} }; class C: virtual public A { public: virtual ~C(){ } virtual void eat(){ std::cout<<"EAT=>C";} }; class D: public B,C { public: virtual ~D(){ } virtual void eat(){ std::cout<<"EAT=>D";} }; int main(int argc, char ** argv){ A *a = new D(); a->eat(); delete a; }
... таким образом вывод будет правильным: "EAT => D"
Виртуальное наследование решает только дублирование дедушки! НО вам все равно нужно указать методы, которые будут виртуальными, чтобы методы были правильно переопределены ...
источник