Виртуальное наследование — это метод C++ , который гарантирует, что только одна копия переменных-членов базового класса наследуется внучатыми производными классами. Без виртуального наследования, если два класса B
и C
наследуют от класса A
, а класс D
наследует от обоих B
и C
, то D
будет содержать две копии переменных- A
членов : одну через B
, и одну через C
. Они будут доступны независимо, используя разрешение области видимости .
Вместо этого, если классы B
и C
виртуально наследуют от класса A
, то объекты класса D
будут содержать только один набор переменных-членов из класса A
.
Эта функция наиболее полезна для множественного наследования , поскольку она делает виртуальную базу общим подобъектом для производного класса и всех классов, которые получены из него. Это можно использовать для избежания проблемы ромба , проясняя неоднозначность относительно того, какой класс-предок использовать, поскольку с точки зрения производного класса ( D
в примере выше) виртуальная база ( A
) действует так, как будто она является прямым базовым классом D
, а не классом, полученным косвенно через базу ( B
или C
). [1] [2]
Он используется, когда наследование представляет собой ограничение набора, а не композицию частей. В C++ базовый класс, который должен быть общим во всей иерархии, обозначается как виртуальный с virtual
ключевым словом .
Рассмотрим следующую иерархию классов.
struct Animal { virtual ~ Animal () = default ; // Явно показываем, что будет создан деструктор класса по умолчанию. virtual void Eat () {} }; структура Млекопитающее : Животное { virtual void Breathe () {} }; struct WingedAnimal : Животное { virtual void Flap () {} }; // Летучая мышь — это крылатое млекопитающее struct Bat : Mammal , WingedAnimal {};
Как было заявлено выше, вызов bat.Eat
неоднозначен, поскольку Animal
в , есть два (косвенных) базовых класса Bat
, поэтому любой Bat
объект имеет два разных Animal
подобъекта базового класса. Таким образом, попытка напрямую привязать ссылку к Animal
подобъекту объекта Bat
потерпит неудачу, поскольку привязка по своей сути неоднозначна:
Летучая мышь летучая мышь ; Животное & животное = летучая мышь ; // ошибка: в какой подобъект Животное следует поместить Летучую мышь, // Mammal::Animal или WingedAnimal::Animal?
Чтобы устранить неоднозначность, придется явно выполнить преобразование bat
в один из подобъектов базового класса:
Летучая мышь летучая мышь ; Животное и млекопитающее = static_cast < Млекопитающее &> ( летучая мышь ); Животное и крылатый = static_cast < КрылатоеЖивотное &> ( летучая мышь );
Для вызова Eat
необходимо то же самое устранение неоднозначности или явная квалификация: static_cast<Mammal&>(bat).Eat()
или static_cast<WingedAnimal&>(bat).Eat()
или альтернативно bat.Mammal::Eat()
и bat.WingedAnimal::Eat()
. Явная квалификация не только использует более простой, единообразный синтаксис для указателей и объектов, но и допускает статическую диспетчеризацию, поэтому, возможно, это будет предпочтительным методом.
В этом случае двойное наследование, Animal
вероятно, нежелательно, так как мы хотим смоделировать, что отношение ( Bat
является Animal
) существует только один раз; то, что a Bat
является Mammal
и является WingedAnimal
, не подразумевает, что оно является Animal
дважды: Animal
базовый класс соответствует контракту, который Bat
реализует (отношение " является " выше на самом деле означает " реализует требования "), а a Bat
реализует Animal
контракт только один раз. Реальное значение " является только один раз" заключается в том, что Bat
должен быть только один способ реализации Eat
, а не два разных способа, в зависимости от того, является ли представление Mammal
ест Bat
или WingedAnimal
представление Bat
. (В первом примере кода мы видим, что Eat
не переопределяется ни в одном из них , Mammal
ни WingedAnimal
, поэтому два Animal
подобъекта фактически будут вести себя одинаково, но это всего лишь вырожденный случай, и это не имеет значения с точки зрения C++.)
Эту ситуацию иногда называют наследованием алмаза (см. Проблема алмаза ), поскольку диаграмма наследования имеет форму алмаза. Виртуальное наследование может помочь решить эту проблему.
Мы можем переобъявить наши классы следующим образом:
struct Animal { virtual ~ Animal () = default ; virtual void Eat () {} }; // Два класса, виртуально наследующие Animal: struct Mammal : virtual Animal { virtual void Breathe () {} }; struct WingedAnimal : виртуальное животное { virtual void Flap () {} }; // Летучая мышь по-прежнему является крылатым млекопитающим struct Bat : Mammal , WingedAnimal {};
Часть теперь является тем жеAnimal
экземпляром , что и тот, который используется , то есть a имеет только один, общий, экземпляр в своем представлении, и поэтому вызов to однозначен. Кроме того, прямое приведение из to также однозначно, теперь существует только один экземпляр, в который можно преобразовать.Bat::WingedAnimal
Animal
Bat::Mammal
Bat
Animal
Bat::Eat
Bat
Animal
Animal
Bat
Возможность совместного использования одного экземпляра родителя Animal
между Mammal
и WingedAnimal
включается путем записи смещения памяти между членами Mammal
или WingedAnimal
и членами базы Animal
в производном классе. Однако это смещение в общем случае может быть известно только во время выполнения, поэтому Bat
должно стать ( vpointer
, Mammal
, vpointer
, WingedAnimal
, Bat
, Animal
). Существует два указателя vtable , по одному на иерархию наследования, которая виртуально наследует Animal
. В этом примере один для Mammal
и один для WingedAnimal
. Таким образом, размер объекта увеличился на два указателя, но теперь есть только один Animal
и нет неоднозначности. Все объекты типа Bat
будут использовать одни и те же vpointers, но каждый Bat
объект будет содержать свой собственный уникальный Animal
объект. Если другой класс наследует от Mammal
, например Squirrel
, то vpointer в Mammal
части Squirrel
будет, как правило, отличаться от vpointer в Mammal
части , Bat
хотя они могут оказаться одинаковыми, если Squirrel
класс имеет тот же размер, что и Bat
.
Этот пример иллюстрирует случай, когда базовый класс A
имеет переменную-конструктор msg
, а дополнительный предок E
получен из внучатого класса D
.
А / \ до нашей эры \ / Д | Э
Здесь, A
должно быть построено в обоих D
и E
. Кроме того, проверка переменной msg
иллюстрирует, как класс A
становится прямым базовым классом своего производного класса, в отличие от базового класса любого промежуточного производного класса, классифицированного между A
и конечным производным классом. Код ниже можно изучить интерактивно здесь.
#include <строка> #include <поток_io> класс A { private : std :: string _msg ; public : A ( std :: string x ) : _msg ( x ) {} void test () { std :: cout << "привет от A: " << _msg << " \n " ; } }; // B,C виртуально наследуют A class B : virtual public A { public : B () : A ( "b" ){} }; class C : virtual public A { public : C () : A ( "c" ){} }; // поскольку B,C виртуально наследуют A, A должен быть создан в каждом дочернем элементе // конструкторы B() и C() могут быть опущены class D : public B , C { public : D () : A ( "d_a" ), B (), C (){} }; // конструктор D() опущен class E : public D { public : E () : A ( "e_a" ){} }; // ломается без построения A // класс D: public B,C { public: D():B(),C(){} };// ломается без построения A //class E: public D { public: E():D(){} };int main ( int argc , char ** argv ) { D d ; d . test (); // выводит: "привет от A: d_a" E e ; e . test (); // выводит: "привет от A: e_a" }
Предположим, что в базовом классе определен чистый виртуальный метод. Если производный класс виртуально наследует базовый класс, то чистый виртуальный метод не обязательно должен быть определен в этом производном классе. Однако, если производный класс не наследует базовый класс виртуально, то все виртуальные методы должны быть определены. Код ниже можно изучить интерактивно здесь.
#include <строка> #include <поток_io> класс A { protected : std :: string _msg ; public : A ( std :: string x ) : _msg ( x ) {} void test () { std :: cout << "привет от A: " << _msg << " \n " ; } virtual void pure_virtual_test () = 0 ; }; // поскольку B,C виртуально наследуют A, то чисто виртуальный метод pure_virtual_test не нужно определять class B : virtual public A { public : B ( std :: string x ) : A ( "b" ){} }; class C : virtual public A { public : C ( std :: string x ) : A ( "c" ){} }; // поскольку B,C виртуально наследуют A, A должен быть сконструирован в каждом дочернем элементе // однако, поскольку D виртуально не наследует B,C, чистый виртуальный метод в A *должен быть определен* class D : public B , C { public : D ( std :: string x ) : A ( "d_a" ), B ( "d_b" ), C ( "d_c" ){} void pure_virtual_test () override { std :: cout << "чистый виртуальный привет от: " << _msg << " \n " ; } }; // нет необходимости переопределять чисто виртуальный метод после того, как родительский класс его определил class E : public D { public : E ( std :: string x ) : A ( "e_a" ), D ( "e_d" ){} }; int main ( int argc , char ** argv ) { D d ( "d" ); d . test (); // привет от A: d_a d . pure_virtual_test (); // чисто виртуальный привет от: d_a E e ( "e" ); e.test ( ); // привет от A: e_a e.pure_virtual_test ( ) ; // чисто виртуальный привет от: e_a }
Одной из проблем, возникающих из-за множественного наследования, является проблема ромба. Классическая иллюстрация этого приведена Бьярне Страуструпом (создателем C++) в следующем примере:
Это то, что может потребоваться, если вы используете множественное наследование. В этом случае класс может быть получен из других классов, имеющих тот же базовый класс. В таких случаях без виртуального наследования ваши объекты будут содержать более одного подобъекта базового типа, который разделяют базовые классы. Является ли это требуемым эффектом, зависит от обстоятельств. Если это не так, то вы можете использовать виртуальное наследование, указав виртуальные базовые классы для тех базовых типов, для которых весь объект должен содержать только один такой подобъект базового класса.