Как виртуальное наследование решает “ромб” (множественное наследование) неоднозначность?

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, но это имеет два пути, за которыми это может следовать, но не может решить отдельно.

Так, как виртуальное наследование решает вопрос (компилятор справки принимают решение)?

85
задан curiousguy 23 June 2016 в 01:55
поделиться

3 ответа

Вы хотите: (достижимо с виртуальным наследованием)

  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 становится неоднозначным, и вам нужно указать, по какому пути вы хотите пойти.

В Википедии есть еще одно хорошее изложение и пример здесь

98
ответ дан 24 November 2019 в 08:16
поделиться

Проблема не в пути , по которому должен следовать компилятор. Проблема заключается в конечной точке этого пути: результат приведения. Когда дело доходит до преобразования типов, путь не имеет значения, имеет значение только конечный результат.

Если вы используете обычное наследование, каждый путь имеет свою отличительную конечную точку, что означает, что результат приведения неоднозначен, что является проблемой.

Если вы используете виртуальное наследование, вы получаете ромбовидную иерархию: оба пути ведут к одной и той же конечной точке. В этом случае проблема выбора пути отпадает (или, точнее, перестает иметь значение), потому что оба пути приводят к одному и тому же результату. Результат больше не однозначный - вот что важно. Точный путь - нет.

9
ответ дан 24 November 2019 в 08:16
поделиться

Экземпляры производных классов «содержат» экземпляры базовых классов, поэтому они выглядят в памяти следующим образом:

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
43
ответ дан 24 November 2019 в 08:16
поделиться
Другие вопросы по тегам:

Похожие вопросы: