Что такое виртуальное наследование
Перейти к содержимому

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

  • автор:

Лекция N. Множественное наследование, «дружба»

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

struct D: A,B,C < >;

Конструкторы, деструкторы

От порядка родителей зависит порядок вызова конструкторов и деструкторов.

Как использовать множественное наследоание

В основном множественное наследование используется при наследовании от интерфейсов. Интерфейс в C++ это класс, у которого все функции виртуальные. Наследование реализации применяется очень редко.

Пример наследования реализации:

Мы видим что второй вариант с одиночным наследованием проще и логичнее.

Пример злоупотребления:

struct Circle

Но некоторые делают:

struct Circle:Point

Как мы видим, множественное наследование порой логичнее и проще заменить на включение объекта внутрь нашего класса.

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

Пример наследования от интерфейсов:

Наследование от нескольких интерфейсов является одним из самых оправданных случаев применения множественного наследования.

Другие особенности множественного наследования

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

Способ обойти это ограничение:

Можно явно вызвать виртуальный метод базового класса

struct A < virtual void foo()=0; >; struct B < virtual void foo() < std::cout >; struct C:A,B < virtual void foo() < std::cout >; int main()

Таблица виртуальных функций и конструкторы

Во время последовательного вызова конструкторов при создании объекта класса, внутри тела каждого конструктора указатель на таблицу виртуальных функций будет установлен на тот класс, для которого вызван конструтор. Так как контекст будет изменяться при вызове конструторов, вызывать в них виртуальные функции является плохим тоном.

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

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

При обычно наследовании мы бы получили в классе C две копии класса A. Но если семантика нашего наследования предполагает что C является A, также как B1 и B2, то мы получаем логическое противоречие такой конструкции. Также такая конструкция будет не верна, если нам нужно иметь одну копию класса A в C. Для преодоления этой ситуации в C++ было введено виртуальное наследование. Ключевое слово virtual при наследовании показывает компилятору, что класс наследник может учавствовать в ромбовидных иерархиях.

Вот как будет выглядеть код этого примера с виртуальным наследованием:

struct A < void foo() < std::cout >; struct B1: virtual A < >; struct B2: virtual A < >; struct C:B1,B2 < >; int main()

Как это работает

Компилятор добавляет в таблицу виртуальных функций класса наследника функцию, которая возвращает указатель на объект базового класса. Таким образом эти функции у B1 и B2 будут возвращать один и тот же указатель на объект A, сожержащийся в C. Реализация виртуального наследования не регламентируется, поэтому есть и другие подходы, например, основанный на отдельной таблице виртуальных классов.

«Дружба»

В С++ существует ключевое слово friend, означающее что класс или функция будут видеть все данные и методы класса, с которым они дружат. Если A друг B, то это не значит что B друг A.

Пример (Функция foo Дружит с классом Bar):

struct Bar < friend void foo(Bar& bar); private: static int data_; void MakeCoffee() < >>; void foo(Bar& bar) < bar.MakeCoffee(); std::cout int Bar::data_;

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

По умолчанию наследование в C++ является специальной формой композиции по значению. Когда мы пишем:

class Bear : public ZooAnimal < . >;

каждый объект Bear содержит все нестатические данные-члены подобъекта своего базового класса ZooAnimal, а также нестатические члены, объявленные в самом Bear. Аналогично, если производный класс является базовым для какого-то другого:

class PolarBear : public Bear < . >;

то каждый объект PolarBear содержит все нестатические члены, объявленные в PolarBear, Bear и ZooAnimal.

В случае одиночного наследования эта форма композиции по значению, поддерживаемая механизмом наследования, обеспечивает компактное и эффективное представление объекта. Проблемы возникают только при множественном наследовании, когда некоторый базовый класс неоднократно встречается в иерархии наследования. Самый известный реальный пример такого рода – это иерархия классов iostream. Взгляните еще раз на рис. 18.2: istream и ostream наследуют одному и тому абстрактному базовому классу ios, а iostream является производным как от istream, так и от ostream.

public istream, public ostream < . >;

По умолчанию каждый объект iostream содержит два подобъекта ios: из istream и из ostream. Почему это плохо? С точки зрения эффективности хранение двух копий подобъекта ios – пустая трата памяти, поскольку объекту iostream нужен только один экземпляр. Кроме того, конструктор вызывается для каждого подобъекта. Более серьезной проблемой является неоднозначность, к которой приводит наличие двух экземпляров. Например, любое неквалифицированное обращение к члену класса ios дает ошибку компиляции. Какой экземпляр имеется в виду? Что будет, если классы istream и ostream инициализируют свои подобъекты ios по-разному? Можно ли гарантировать, что в классе iostream используется согласованная пара членов ios? Применяемый по умолчанию механизм композиции по значению не дает таких гарантий.

Для решения данной проблемы язык предоставляет альтернативный механизм композиции по ссылке: виртуальное наследование. В этом случае наследуется только один разделяемый подобъект базового класса, независимо от того, сколько раз базовый класс встречается в иерархии наследования. Этот разделяемый подобъект называется виртуальным базовым классом. С помощью виртуального наследования снимаются проблемы дублирования подобъектов базового класса и неоднозначностей, к которым такое дублирование приводит.

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

class Panda : public Bear,

public Raccoon, public Endangered < . >;

Наша виртуальная иерархия наследования Panda показана на рис. 18.4: две пунктирные стрелки обозначают виртуальное наследование классов Bear и Raccoon от ZooAnimal, а три сплошные – невиртуальное наследование Panda от Bear, Raccoon и, на всякий случай, от класса Endangered из раздела 18.2.

Рис. 18.4. Иерархия виртуального наследования класса Panda

На данном рисунке показан интуитивно неочевидный аспект виртуального наследования: оно (в нашем случае наследование классов Bear и Raccoon) должно появиться в иерархии раньше, чем в нем возникнет реальная необходимость. Необходимым виртуальное наследование становится только при объявлении класса Panda, но если перед этим базовые классы Bear и Raccoon не наследуют своему базовому виртуально, то проектировщику класса Panda не повезло.

Должны ли мы производить свои базовые классы виртуально просто потому, что где-то ниже в иерархии может потребоваться виртуальное наследование? Нет, это не рекомендуется: снижение производительности и усложнение дальнейшего наследования может оказаться существенным (см. [LIPPMAN96a], где приведены и обсуждаются результаты измерения производительности).

Когда же использовать виртуальное наследование? Чтобы его применение было успешным, иерархия, например библиотека iostream или наше дерево классов Panda, должна проектироваться целиком либо одним человеком, либо коллективом разработчиков.

В общем случае мы не рекомендуем пользоваться виртуальным наследованием, если только оно не решает конкретную проблему проектирования. Однако посмотрим, как все-таки можно его применить.

18.5.1. Объявление виртуального базового класса

Для указания виртуального наследования в объявление базового класса вставляется модификатор virtual. Так, в данном примере ZooAnimal становится виртуальным базовым для Bear и Raccoon:

// взаимное расположение ключевых слов public и virtual

class Bear : public virtual ZooAnimal < . >;

class Raccoon : virtual public ZooAnimal < . >;

Виртуальное наследование не является явной характеристикой самого базового класса, а лишь описывает его отношение к производному. Как мы уже отмечали, виртуальное наследование – это разновидность композиции по ссылке. Иначе говоря, доступ к подобъекту и его нестатическим членам косвенный, что обеспечивает гибкость, необходимую для объединения нескольких виртуально унаследованных подобъектов базовых классов в один разделяемый экземпляр внутри производного. В то же время объектом производного класса можно манипулировать через указатель или ссылку на тип базового, хотя последний является виртуальным. Например, все показанные ниже преобразования базовых классов Panda выполняются корректно, хотя Panda использует виртуальное наследование:

extern void dance( const Bear* );

extern void rummage( const Raccoon* );

cout yin_yang; // правильно

Любой класс, который можно задать в качестве базового, разрешается сделать виртуальным, причем он способен содержать все те же элементы, что обычные базовые классы. Так выглядит объявление ZooAnimal:

ZooAnimal( string name,

bool onExhibit, string fam_name )

_onExhibit( onExhibit ), _fam_name( fam_name )

string name() const

string family_name() const

К объявлению и реализации непосредственного базового класса при использовании виртуального наследования добавляется ключевое слово virtual. Вот, например, объявление нашего класса Bear:

class Bear : public virtual ZooAnimal

two_left_feet, macarena, fandango, waltz >;

Bear( string name, bool onExhibit=true )

: ZooAnimal( name, onExhibit, "Bear" ),

void dance( DanceType );

А вот объявление класса Raccoon:

class Raccoon : public virtual ZooAnimal

Raccoon( string name, bool onExhibit=true )

: ZooAnimal( name, onExhibit, "Raccoon" ),

bool pettable() const

void pettable( bool petval )

Читайте также

2. Наследование

2. Наследование Процесс, с помощью которого один тип наследует характеристики другого типа, называется наследованием. Наследник называется порожденным (дочерним) типом, а тип, которому наследует дочерний тип, называется порождающим (родительским) типом.Ранее известные

Правило 34: Различайте наследование интерфейса и наследование реализации

Правило 34: Различайте наследование интерфейса и наследование реализации Внешне простая идея открытого наследования при ближайшем рассмотрении оказывается состоящей из двух различных частей: наследования интерфейса функций и наследования их реализации. Различие

1.1.2. Наследование

1.1.2. Наследование Мы подходим к одной из самых сильных сторон ООП — наследованию. Наследование —- это механизм, позволяющий расширять ранее определенную сущность путем добавления новых возможностей. Короче говоря, наследование - это способ повторного использования

Наследование

Наследование Следующим принципом ООП является наследование, означающее способность языка обеспечить построение определений новых классов на основе определений существующих классов. В сущности, наследование позволяет расширить возможности поведения базового класса

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

Виртуальное пространство Работа над трехмерными интерьерами и другими проектами происходит в виртуальном пространстве. Термин "виртуальность" пришел к нам от английского "virtual", что в переводе означает "возможный, воображаемый, существующий лишь как продукт

18. Множественное и виртуальное наследование

18. Множественное и виртуальное наследование В большинстве реальных приложений на C++ используется открытое наследование от одного базового класса. Можно предположить, что и в наших программах оно в основном будет применяться именно так. Но иногда одиночного наследования

Наследование

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

Множественное наследование

Множественное наследование Множественное наследование выполняется подобно единичному наследованию. В отличие от единичного наследования у порожденного класса может быть несколько базовых классов. На рисунке 1.2 представлен пример множественного наследования классов.

Наследование

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

Smarter Objects: виртуальное взаимодействие с реальными объектами Николай Маслухин

Smarter Objects: виртуальное взаимодействие с реальными объектами Николай Маслухин Опубликовано 07 мая 2013 Медиалаборатория Массачусетского технологического института (MIT Media Lab) представила новую технологию взаимодействия с физическими объектами на

РЕПОРТАЖ: Виртуальное присутствие

РЕПОРТАЖ: Виртуальное присутствие Москву посетил Никлаус Вирт (Niclaus Wirth). Известен он в России прежде всего как создатель языка Pascal. Знаменитый профессор Высшей политехнической школы в Цюрихе (ETH; в ней, кстати, учились Альберт Эйнштейн и Джон фон Нейман) и директор

ООН создала виртуальное минное поле при помощи iBeacon Николай Маслухин

ООН создала виртуальное минное поле при помощи iBeacon Николай Маслухин Опубликовано 08 апреля 2014 4 апреля, в международный день «просвещения по вопросам минной опасности и помощи в деятельности, связанной с разминированием», Организация

Виртуальное окно в БМП или как студенты-дизайнеры апгрейдили броневик Николай Маслухин

Виртуальное окно в БМП или как студенты-дизайнеры апгрейдили броневик Николай Маслухин Опубликовано 12 апреля 2013 M2 Bradley, боевая машина пехоты США, была создана еще в 70-х годах, но используется армией и поныне. По заявлению самих же военных, одним из

26. Наследование

26. Наследование Наследование – это процесс порождения новых типов-потомков от существующих типов-родителей, при этом потомок получает (наследует) от родителя все его поля и методы.Тип-потомок, при этом, называется наследником или порожденным (дочерним) типом. А тип,

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

Виртуа́льное насле́дование (англ. virtual inheritance ) в языке программирования C++ — один из вариантов наследования, который нужен для решения некоторых проблем, порождаемых наличием возможности множественного наследования (особенно «ромбовидного наследования»), путем разрешения неоднозначности того, методы которого из суперклассов (непосредственных классов-предков) необходимо использовать. Оно применяется в тех случаях, когда множественное наследование вместо предполагаемой полной композиции свойств классов-предков приводит к ограничению доступных наследуемых свойств вследствие неоднозначности. Базовый класс, наследуемый множественно, определяется виртуальным с помощью ключевого слова virtual .

Суть проблемы

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

class Animal  public: virtual void eat(); // Метод определяется для данного класса >; class Mammal : public Animal  public: virtual Color getHairColor(); >; class WingedAnimal : public Animal  public: virtual void flap(); >; // A bat is a winged mammal class Bat : public Mammal, public WingedAnimal >; // Bat bat; 

Но как летучая мышь ест? Что происходит при вызове метода bat.eat() ?

В вышеприведенном коде вызов bat.eat() является неоднозначным. Он может относиться как к Bat::WingedAnimal::Animal::eat() так и к Bat::Mammal::Animal::eat() . И у каждого наследника (WingedAnimal, Mammal) метод eat() определен по-своему. Проблема в том, что семантика традиционного множественного наследования не соответствует моделируемой им реальности. В некотором смысле, сущность Animal единственна по сути; Bat — это Mammal и WingedAnimal , но свойство животности ( Animal ness) летучей мыши ( Bat ), оно же свойство животности млекопитающего ( Mammal ) и оно же свойство животности WingedAnimal — по сути это одно и то же свойство.

Такая ситуация обычно именуется diamond inheritance («ромбическое наследование») и представляет из себя проблему, которую призвано решить виртуальное наследование.

Представление класса

Прежде чем продолжить, полезным будет рассмотреть, как классы представляются в C++. В частности, при наследовании классы предка и наследника просто помещаются в памяти друг за другом. Таким образом объект класса Bat это на самом деле последовательность объектов классов (Animal,Mammal,Animal,WingedAnimal,Bat), размещенных последовательно в памяти, при этом Animal повторяется дважды, что и приводит к неоднозначности.

Решение

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

class Animal  public: virtual void eat(); >; // Two classes virtually inheriting Animal: class Mammal : public virtual Animal //  public: virtual Color getHairColor(); >; class WingedAnimal : public virtual Animal //  public: virtual void flap(); >; // A bat is still a winged mammal class Bat : public Mammal, public WingedAnimal >; 

Теперь, часть Animal объекта класса Bat::WingedAnimal та же самая, что и часть Animal , которая используется в Bat::Mammal , и можно сказать, что Bat имеет в своем представлении только одну часть Animal и вызов Bat::eat() становится однозначным.

Виртуальное наследование реализуется через добавление указателей на виртуальную таблицу vtable в классы Mammal и WingedAnimal , это делается в частности потому, что смещение памяти между началом Mammal и его Animal части неизвестно на этапе компиляции, а выясняется только во время выполнения. Таким образом, Bat представляется, как (vtable*, Mammal, vtable*, WingedAnimal, Bat, Animal). Два указателя vtable на объект увеличивают размер объекта на величину двух указателей, но это обеспечивает единственность Animal и отсутствие многозначности. Нужны два указателя vtables: по одному на каждого предка в иерархии, который виртуально наследуется от Animal : один для Mammal и один для WingedAnimal . Все объекты класса Bat будут иметь одни и те же указатели vtable *, но каждый отдельный объект Bat будет содержать собственную реализацию объекта Animal . Если какой-нибудь другой класс будет наследоваться от Mammal , например Squirrel (белка), то vtable* в объекте Mammal объекта Squirrel будет отличаться от vtable* в объекте Mammal объекта Bat , хотя в особом случае они по-прежнему могут быть одинаковы по сути: когда часть Squirrel объекта имеет тот же самый размер, что и часть Bat , поскольку, тогда расстояние от реализации Mammal до части Animal будет одинаковым. Но сами виртуальные таблицы vtables будут все же разными, в отличие от располагаемой в них информации о смещениях.

Пример

Чтобы понять суть виртуального наследование без лишнего "шума", следует рассмотреть следующий пример:

#include class A  public: int foo()  return 1; > >; class B: public virtual A  public: >; class C : public virtual A  public: >; class D : public B, public C >; int main()  D d; std::cout  foo(); return 0; > 

Если убрать ключевое слово virtual, то метод foo() не будет доступен как объект класса D и код не скомпилируется.

См. также

  • Объектно-ориентированное программирование
  • Наследование

Литература

  • Подбельский В. В. Глава 10.2 Множественное наследование и виртуальные базовые классы // Язык Си++ / рец. Дадаев Ю. Г.. — 4. — М .: Финансы и статистика, 2003. — С. 336-359. — 560 с. — ISBN 5-279-02204-7, УДК 004.438Си(075.8) ББК 32.973.26-018 1я173
  • C++
  • Наследование (программирование)

Грабли 2: Виртуальное наследование

Статья о том, как множественное наследование все усложняет. Как виртуальное наследование, на первый взгляд, реализовано нелогично. Как на второй взгляд логика появляется, но уровень сложности и запутанности продолжает расти. В общем, чем сложнее задача, тем более простые нужно подбирать инструменты.

Все основано на реальных событиях, но примеры были максимально упрощены, чтобы в них осталась лишь суть проблемы.

Итак, в разрабатываемом приложении использовалось большое количество единообразно обрабатываемых сущностей. Причем, одни из них были отображаемыми, другие требовалось постоянно обновлять, третьи соединяли в себе и то и другое. Соответственно появилось желание реализовать три базовых класса

  1. Renderable: содержит признак видимости и метод рисования
  2. Updatable: содержит признак активности и метод обновления состояния
  3. VisualActivity = Renderable + Updatable

Добавлю еще два искусственных класса, чтобы продемонстрировать случившиеся сложности

  1. JustVisible: просто видимый объект
  2. JustVisiblePlusVisualActivity: JustVisible с обновляемым состоянием

Получается следующая картина

Сразу же видна проблема — конечный класс наследует Renderable дважды: как родитель JustVisible и VisualActivity. Это не дает нормально работать со списками отображаемых объектов

 JustVisiblePlusUpdate* object = new JustVisiblePlusUpdate; std::vector vector_visible; vector_visible.push_back(object); 

Получается неоднозначность (ambiguous conversions) — компилятор не может понять, об унаследованном по какой ветке Renderable идет речь. Ему можно помочь, уточнив направление путем явного приведения типа к одному из промежуточных

 JustVisiblePlusUpdate* object = new JustVisiblePlusUpdate; std::vector vector_visible; vector_visible.push_back(static_cast(object)); 

Компиляция пройдет успешно, только вот ошибка останется. В нашем случае требовался один и тот же Renderable вне зависимости от того, каким образом он был унаследован. Дело в том, что в случае обычного наследования в классе-потомке (JustVisiblePlusVisualActivity) содержится отдельный экземпляр родительского класса для каждой ветки.

Причем свойства каждого из них можно менять независимо. Выражаясь на c++, истинно выражение

(&static_cast(object)->mVisible) != (&static_cast(object)->mVisible) 

Так что обычное множественное наследование для задачи не подходило. А вот виртуальное выглядело той самой серебряной пулей, которая была нужна… Все что требовалось — унаследовать базовые классы Renderable и Updatable виртуально, а остальные — обычным образом:

class VisualActivity : public virtual Updatable, public virtual Renderable . class JustVisible : public virtual Renderable . class JustVisiblePlusUpdate : public JustVisible, public VisualActivity 

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

class Updatable < public: Updatable() : mActive(true) < >Updatable(bool active) : mActive(active) < >//. >; 
class Renderable < public: Renderable() : mVisible(true) < >Renderable(bool visible) : mVisible(visible) < >//. >; 

Классы-потомки содержали только конструкторы с параметрами

class VisualActivity : public virtual Updatable, public virtual Renderable < public: VisualActivity(bool visible, bool active) : Renderable(visible) , Updatable(active) < >//. >; 
class JustVisible : public virtual Renderable < public: JustVisible(bool visible) : Renderable(visible) < >//. >; 
class JustVisiblePlusUpdate : public JustVisible, public VisualActivity < public: JustVisiblePlusUpdate(bool visible, bool active) : JustVisible(visible) , VisualActivity(visible, active) < >//. >; 

И все равно при создании объекта

 JustVisiblePlusUpdate* object = new JustVisiblePlusUpdate(false, false); 

вызывался конструктор Renderable по умолчанию! На первый взгляд, это казалось чем-то диким. Но рассмотрим подробнее, откуда взялось предположение, что приведенный код должен приводить к вызову конструктора Renderable::Renderable(bool visible) вместо Renderable::Renderable().

Породило проблему допущение, что Renderable чудесным образом разделится между JustVisible, VisualActivity и JustVisiblePlusUpdate. Но «чуду» не суждено было случиться. Ведь тогда можно было бы написать что-то типа

class JustVisiblePlusUpdate : public JustVisible, public VisualActivity < public: JustVisiblePlusUpdate(bool active) : JustVisible(true) , VisualActivity(false, active) < >//. >; 

сообщив компилятору противоречивую информацию, когда одновременно требовалось бы конструирование Renderable с параметрами true и false. Открывать возможность для подобных парадоксов никто не захотел, соответственно и механизм работает другим образом. Класс Renderable в нашем случае больше не является частью ни JustVisible, ни VisualActivity, а принадлежит непосредственно JustVisiblePlusUpdate.

Это объясняет, почему вызывался конструктор по умолчанию — конструкторы виртуальных классов должны вызываться конечными наследниками, т.е. рабочим вариантом было бы что-то типа

class JustVisiblePlusUpdate : public JustVisible, public VisualActivity < public: JustVisiblePlusUpdate(bool visible, bool active) : JustVisible(visible) , VisualActivity(visible, active) , Renderable(visible) , Updatable(active) < >//. >; 

При виртуальном наследовании приходится, кроме конструкторов непосредственных родителей, явно вызывать конструкторы всех виртуально унаследованных классов. Это не очень очевидно и с легкостью может быть упущено в нетривиальном проекте. Так что лишний раз подтвердилась истина: не больше одного открытого наследования для каждого класса. Оно того не стоит. В нашем случае было принято решение отказаться от разделения на Renderable и Updatable, ограничившись одним базовым VisualActivity. Это добавило некоторую избыточность, но резко упростило общую архитектуру — отслеживать и поддерживать все виртуальные и обычные случаи наследования было слишком затратно.

  • наследование
  • inheritance
  • kiss
  • множественное наследование
  • Программирование
  • C++
  • Проектирование и рефакторинг

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *