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
, но это имеет два пути, за которыми это может следовать, но не может решить отдельно.
Так, как виртуальное наследование решает вопрос (компилятор справки принимают решение)?
Вы хотите: (достижимо с виртуальным наследованием)
A
/ \
B C
\ /
D
А не: (Что происходит без виртуального наследования)
A A
| |
B C
\ /
D
Виртуальное наследование означает, что будет только 1 экземпляр базового класса A
, а не 2.
Ваш тип D
будет иметь 2 указателя vtable (вы можете увидеть их на первой диаграмме), один для B
и один для C
, которые фактически наследуют A
. Размер объекта D
увеличен, потому что теперь он хранит 2 указателя; однако сейчас есть только один A
.
Итак, B :: A
и C :: A
одинаковы, и поэтому не может быть неоднозначных вызовов от D
. Если вы не используете виртуальное наследование, у вас есть вторая диаграмма выше. И тогда любой вызов члена A становится неоднозначным, и вам нужно указать, по какому пути вы хотите пойти.
Проблема не в пути , по которому должен следовать компилятор. Проблема заключается в конечной точке этого пути: результат приведения. Когда дело доходит до преобразования типов, путь не имеет значения, имеет значение только конечный результат.
Если вы используете обычное наследование, каждый путь имеет свою отличительную конечную точку, что означает, что результат приведения неоднозначен, что является проблемой.
Если вы используете виртуальное наследование, вы получаете ромбовидную иерархию: оба пути ведут к одной и той же конечной точке. В этом случае проблема выбора пути отпадает (или, точнее, перестает иметь значение), потому что оба пути приводят к одному и тому же результату. Результат больше не однозначный - вот что важно. Точный путь - нет.
Экземпляры производных классов «содержат» экземпляры базовых классов, поэтому они выглядят в памяти следующим образом:
class A: [A fields]
class B: [A fields | B fields]
class C: [A fields | C fields]
Таким образом, без виртуального наследования экземпляр класса D будет выглядеть так:
class D: [A fields | B fields | A fields | C fields | D fields]
'- derived from B -' '- derived from C -'
Итак, обратите внимание на две «копии» данных A. Виртуальное наследование означает, что внутри производного класса во время выполнения установлен указатель vtable, который указывает на данные базового класса, так что экземпляры классов B, C и D выглядят следующим образом:
class B: [A fields | B fields]
^---------- pointer to A
class C: [A fields | C fields]
^---------- pointer to A
class D: [A fields | B fields | C fields | D fields]
^---------- pointer to B::A
^--------------------- pointer to C::A