Виртуальное наследование - это метод 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
или static_cast
или альтернативно 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
.