stringtranslate.com

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

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

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

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

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

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 AnimalBat::MammalBatAnimalBat::EatBatAnimalAnimalBat

Возможность совместного использования одного экземпляра родителя 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 }      

Ссылки

  1. ^ Милеа, Андрей. "Решение проблемы ромба с помощью виртуального наследования". Cprogramming.com . Получено 2010-03-08 . Одной из проблем, возникающих из-за множественного наследования, является проблема ромба. Классическая иллюстрация этого приведена Бьярне Страуструпом (создателем C++) в следующем примере:
  2. ^ McArdell, Ralph (2004-02-14). "C++/Что такое виртуальное наследование?". Все эксперты . Архивировано из оригинала 2010-01-10 . Получено 2010-03-08 . Это то, что может потребоваться, если вы используете множественное наследование. В этом случае класс может быть получен из других классов, имеющих тот же базовый класс. В таких случаях без виртуального наследования ваши объекты будут содержать более одного подобъекта базового типа, который разделяют базовые классы. Является ли это требуемым эффектом, зависит от обстоятельств. Если это не так, то вы можете использовать виртуальное наследование, указав виртуальные базовые классы для тех базовых типов, для которых весь объект должен содержать только один такой подобъект базового класса.