Виртуальное наследование

редактировать
Диаграмма алмазного наследования, проблема, которую пытается решить виртуальное наследование

Виртуальное наследование - это метод C ++, который гарантирует, что только одна копия переменных-членов базового класса наследуется производными классами внуков. Без виртуального наследования, если два класса Bи Cнаследуются от класса A, а класс Dнаследуется от обоих Bи C, тогда Dбудет содержать две копии переменных-членов A: одну через Bи одну через C. Они будут доступны независимо, с использованием разрешения области.

. Вместо этого, если классы Bи Cнаследуются виртуально от класса A, тогда объекты класса Dбудет содержать только один набор переменных-членов из класса A.

Эта функция наиболее полезна для множественного наследования, поскольку она делает виртуальную базу общим подобъектом для производного класса и всех классов, которые являются производными от него. Это можно использовать, чтобы избежать проблемы ромба, прояснив неоднозначность того, какой класс-предок использовать, с точки зрения производного класса (Dв приведенном выше примере) виртуальная база ( A) действует так, как если бы это был прямой базовый класс D, а не класс, производный косвенно через базовый (Bили C).

Он используется, когда наследование представляет собой ограничение набора, а не композицию частей. В C ++ базовый класс, предназначенный для использования во всей иерархии, обозначается как виртуальный с помощью ключевого слова virtual.

Рассмотрим следующую иерархию классов.

struct Animal {virtual ~ Animal () = default; virtual void Eat () {}}; struct Mammal: Animal {virtual void Breathe () {}}; struct WingedAnimal: Animal {virtual void Flap () {} }; // Летучая мышь - это крылатое млекопитающее struct Bat: Mammal, WingedAnimal {}; Bat bat;

Как указано выше, вызов bat.Eatнеоднозначен, потому что существует два Базовые классы Animal(косвенные) в Bat, поэтому любой объект Batимеет два разных подобъекта базового класса Animal. Таким образом, попытка напрямую привязать ссылку к подобъекту Animalобъекта Batне удастся, так как привязка по своей сути неоднозначна:

Bat b; Животное a = b; // ошибка: в какой подобъект Animal следует использовать Bat, // Mammal :: Animal или WingedAnimal :: Animal?

Чтобы устранить неоднозначность, нужно было бы явно преобразовать batв любой подобъект базового класса:

Bat b; Животное и млекопитающее = static_cast (b); Животное и крылатое = static_cast (b);

Для вызова Eatтребуется такое же разрешение неоднозначности или явная квалификация: static_cast (bat).Eat ()или static_cast (bat).Eat ()или альтернативно bat.Mammal :: Eat ()и bat.WingedAnimal :: Eat (). Явная квалификация не только использует более простой и единообразный синтаксис как для указателей, так и для объектов, но также допускает статическую отправку, поэтому, возможно, это будет предпочтительный метод.

В этом случае двойное наследование Animal, вероятно, нежелательно, поскольку мы хотим смоделировать, что отношение (Batявляется Animal) существует только один раз; то, что Летучая мышьявляется Млекопитающими является WingedAnimal, не означает, что это Животноедважды: Животноебазовый класс соответствует контракту, который реализует Bat(указанное выше отношение "является" действительно означает "реализует требования"), а Batреализует только Животноезаключило контракт один раз. В реальном мире значение «только один раз» состоит в том, что Batдолжен иметь только один способ реализации Eat, а не два разных способа, в зависимости от того, Mammalвид Летучая мышьест или Крылатое животноевид Летучая мышь. (В первом примере кода мы видим, что Eatне переопределяется ни в Mammal, ни в WingedAnimal, поэтому два подобъекта Animalфактически ведут себя так же, но это просто вырожденный случай, и это не имеет значения с точки зрения C ++.)

Эта ситуация иногда упоминается как наследование алмаза (см. Проблема с ромбиком ), потому что диаграмма наследования имеет форму ромба. Виртуальное наследование может помочь решить эту проблему.

Решение

Мы можем повторно объявить наши классы следующим образом:

struct Animal {virtual ~ Animal () = default; virtual void Eat () {}}; // Два класса, виртуально наследующие Animal: struct Mammal: virtual Animal {virtual void Breathe () {}}; struct WingedAnimal: virtual Animal {virtual void Flap () {}}; // Летучая мышь по-прежнему является крылатым млекопитающим struct Bat: Mammal, WingedAnimal {};

Часть Animalв Bat :: WingedAnimalтеперь тот же экземпляр Animal, что и тот, который используется Bat :: Mammal, что означает, что Batимеет только один общий экземпляр Animalв своем представлении, и поэтому вызов Bat :: Eatявляется однозначным. Кроме того, прямое приведение от Batк Animalтакже однозначно, теперь, когда существует только один экземпляр Animal, который Batможно преобразовать к.

Возможность совместного использования одного экземпляра родительского элемента Animalмежду Mammalи WingedAnimalвключается путем записи смещения памяти между Mammalили WingedAnimalи члены базового Animalв производном классе. Однако в общем случае это смещение может быть известно только во время выполнения, поэтому Batдолжен стать (vpointer, Mammal, vpointer, Крылатое животное, Летучая мышь, Животное). Есть два указателя vtable, по одному на иерархию наследования, которая фактически наследует Animal. В этом примере один для Mammalи один для WingedAnimal. Таким образом, размер объекта увеличился на два указателя, но теперь есть только один Animalи никакой двусмысленности. Все объекты типа Batбудут использовать одни и те же vpointers, но каждый объект Batбудет содержать свой собственный уникальный объект Animal. Если другой класс наследуется от Mammal, например Squirrel, то vpointer в части Mammalв Squirrelобычно будет отличаться от vpointer в части Mammalв Bat, хотя они могут оказаться одинаковыми, если класс Squirrelбудет того же размера, что и Bat.

Ссылки
Последняя правка сделана 2021-06-18 03:36:41
Содержание доступно по лицензии CC BY-SA 3.0 (если не указано иное).
Обратная связь: support@alphapedia.ru
Соглашение
О проекте