Как можно решить проблему ромбовидного наследования без использования виртуального наследования

Обновлено: 02.07.2024

По умолчанию наследование в 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: виртуальное взаимодействие с реальными объектами Николай Маслухин

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

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

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

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

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

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

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

Допустим, мы хотим написать программу, чтобы отслеживать группу преподавателей. Преподаватель – это человек. Однако преподаватель также является сотрудником (и, если он работает на себя, является работодателем). Множественное наследование можно использовать для создания класса Teacher (преподаватель), который наследует свойства как от Person (человек), так и от Employee (сотрудник). Чтобы использовать множественное наследование, просто укажите все базовые классы (как и в одиночном наследовании), разделив их запятыми.

Рисунок 1 Диаграмма наследования

Рисунок 1 – Диаграмма наследования

Проблемы с множественным наследованием

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

Во-первых, неоднозначность может возникнуть, если несколько базовых классов содержат функцию с одним и тем же именем. Например:

Когда c54G.getID() компилируется, компилятор проверяет, содержит ли WirelessAdapter функцию с именем getID() . Ее у него нет. Затем компилятор проверяет, есть ли в каком-либо из родительских классов функция с именем getID() . Видите здесь проблему? Проблема в том, что c54G на самом деле содержит ДВЕ функции getID() : одна унаследована от USBDevice , а другая – от NetworkDevice . Следовательно, этот вызов функции неоднозначен, и вы получите ошибку компиляции, если попытаетесь скомпилировать этот код.

Однако есть способ обойти эту проблему: вы можете явно указать, какую версию вы хотели вызвать:

Хотя этот обходной путь довольно прост, вы можете увидеть, как всё может усложниться, когда ваш класс наследуется от четырех или шести базовых классов, которые сами наследуются от других классов. По мере того, как вы наследуете больше классов, вероятность конфликтов имен возрастает экспоненциально, и каждый из этих конфликтов имен должен быть разрешен явным образом.

Во-вторых, более серьезная проблема – проблема ромба (или англоязычный термин – diamond problem). Она появляется, когда класс множественно наследуется от двух классов, каждый из которых наследуется от одного базового класса. Это приводит к ромбовидной структуре наследования.

Например, рассмотрим следующий набор классов:

Рисунок 2 Ромбовидная структура наследования

Рисунок 2 – Ромбовидная структура наследования

Сканеры ( Scanner ) и принтеры ( Printer ) являются устройствами с питанием, поэтому они являются производными от PoweredDevice . Однако копировальный аппарат ( Copier ) включает в себя функции как сканеров, так и принтеров.

Множественное наследование – больше проблем, чем оно того стоит?

Многие авторы и опытные программисты считают, что множественного наследования в C++ следует избегать любой ценой из-за множества потенциальных проблем, которые оно приносит. Автор данного руководства не согласен с этим подходом, потому что бывают случаи и ситуации, когда множественное наследование – лучший способ решения задачи. Однако множественное наследование следует использовать крайне осторожно.

Интересно отметить, что вы уже использовали классы, написанные с использованием множественного наследования, даже не подозревая об этом: объекты библиотеки iostream std::cin и std::cout реализованы с использованием множественного наследования!

Правило

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

Множественное наследование в Java. Композиция в сравнении с Наследованием

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

Ромбовидная проблема

Множественное наследование Интерфейсов

Композиция против Наследования

Предположим, что у нас есть суперкласс и класс, расширяющий его:

Обратите внимание, что метод test() уже существует в подклассе, но тип возвращаемого значения отличается. Теперь класс ClassD не будет компилироваться и если вы будете использовать какой-либо IDE, то она вам предложит изменить тип возвращаемого значения в суперклассе или подклассе.

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

Вышеупомянутая проблема никогда не произойдет с композицией и это делает ее более привлекательной по отношению к наследованию.

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

Результат программы представленной выше:

Эта гибкость при вызове метода не доступна при наследовании, что добавляет еще один плюс в пользу выбора композиции.

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

Наследование в C++: beginner, intermediate, advanced

В этой статье наследование описано на трех уровнях: beginner, intermediate и advanced. Expert нет. И ни слова про SOLID. Честно.

Beginner

Что такое наследование?

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

Класс, который наследует данные, называется подклассом (subclass), производным классом (derived class) или дочерним классом (child). Класс, от которого наследуются данные или методы, называется суперклассом (super class), базовым классом (base class) или родительским классом (parent). Термины “родительский” и “дочерний” чрезвычайно полезны для понимания наследования. Как ребенок получает характеристики своих родителей, производный класс получает методы и переменные базового класса.

Наследование полезно, поскольку оно позволяет структурировать и повторно использовать код, что, в свою очередь, может значительно ускорить процесс разработки. Несмотря на это, наследование следует использовать с осторожностью, поскольку большинство изменений в суперклассе затронут все подклассы, что может привести к непредвиденным последствиям.

Важное примечание: приватные переменные и методы не могут быть унаследованы.

Типы наследования

В C ++ есть несколько типов наследования:

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

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

Важное примечание: в этой статье не освещены виртуальные десктрукторы. Дополнительный материал на эту тему можно найти к примеру в этой статье на хабре.

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

Множественное наследование происходит, когда подкласс имеет два или более суперкласса. В этом примере, класс Laptop наследует и Monitor и Computer одновременно.

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

Множественное наследование требует тщательного проектирования, так как может привести к непредвиденным последствиям. Большинство таких последствий вызваны неоднозначностью в наследовании. В данном примере Laptop наследует метод turn_on() от обоих родителей и неясно какой метод должен быть вызван.

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

Intermediate

Проблема ромба

Ромбовидная проблема — прежде всего проблема дизайна, и она должна быть предусмотрена на этапе проектирования. На этапе разработки ее можно разрешить следующим образом:

Проблема ромба: Конструкторы и деструкторы

Поскольку в С++ при инициализации объекта дочернего класса вызываются конструкторы всех родительских классов, возникает и другая проблема: конструктор базового класса Device будет вызван дважды.

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

Виртуальное наследование (virtual inheritance) предотвращает появление множественных объектов базового класса в иерархии наследования. Таким образом, конструктор базового класса Device будет вызван только единожды, а обращение к методу turn_on() без его переопределения в дочернем классе не будет вызывать ошибку при компиляции.

Примечание: виртуальное наследование в классах Computer и Monitor не разрешит ромбовидное наследование если дочерний класс Laptop будет наследовать класс Device не виртуально ( class Laptop: public Computer, public Monitor, public Device <>; ).

Абстрактный класс

В С++, класс в котором существует хотя бы один чистый виртуальный метод (pure virtual) принято считать абстрактным. Если виртуальный метод не переопределен в дочернем классе, код не скомпилируется. Также, в С++ создать объект абстрактного класса невозможно — попытка тоже вызовет ошибку при компиляции.

Интерфейс

С++, в отличии от некоторых ООП языков, не предоставляет отдельного ключевого слова для обозначения интерфейса (interface). Тем не менее, реализация интерфейса возможна путем создания чистого абстрактного класса (pure abstract class) — класса в котором присутствуют только декларации методов. Такие классы также часто называют абстрактными базовыми классами (Abstract Base Class — ABC).

Advanced

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

Наследование от реализованного или частично реализованного класса

Если наследование происходит не от интерфейса (чистого абстрактного класса в контексте С++), а от класса в котором присутствуют какие-либо реализации, стоит учитывать то, что класс наследник связан с родительским классом наиболее тесной из возможных связью. Большинство изменений в классе родителя могут затронуть наследника что может привести к непредвиденному поведению. Такие изменения в поведении наследника не всегда очевидны — ошибка может возникнуть в уже оттестированом и рабочем коде. Данная ситуация усугубляется наличием сложной иерархии классов. Всегда стоит помнить о том, что код может изменяться не только человеком который его написал, и пути наследования очевидные для автора могут быть не учтены его коллегами.

В противовес этому стоит заметить что наследование от частично реализованных классов имеет неоспоримое преимущество. Библиотеки и фреймворки зачастую работают следующим образом: они предоставляют пользователю абстрактный класс с несколькими виртуальными и множеством реализованных методов. Таким образом, наибольшее количество работы уже проделано — сложная логика уже написана, а пользователю остается только кастомизировать готовое решение под свои нужды.

Интерфейс

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

Интерфейс: Пример использования

Прежде всего стоит заметить, что пример тесно связан с понятием полиморфизма, но будет рассмотрен в контексте наследования от чистого абстрактного класса.

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

Отсутствие однозначности касательно форматирования конфигурационного файла не тормозит процесс разработки основной программы. Два разработчика могут работать параллельно — один над бизнес логикой, а другой над парсером. Поскольку они взаимодействуют через этот интерфейс, каждый из них может работать независимо. Данный подход облегчает покрытие кода юнит тестами, так как необходимые тесты могут быть написаны с использованием мока (mock) для этого интерфейса.

Также, при изменении формата конфигурационного файла, бизнес логика приложения не затрагивается. Единственное чего требует полный переход от одного форматирования к другому — написания новой реализации уже существующего абстрактного класса (класса-парсера). В дальнейшем, возврат к изначальному формату файла требует минимальной работы — подмены одного уже существующего парсера другим.

Заключение

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

Наследование: проблемы и альтернативы. Интерфейсы. Композиция

8.1. Проблемы множественного наследования классов. Интерфейсы

Достаточно часто требуется совмещать в объекте поведение, характерное для двух или более независимых иерархий. Но еще чаще требуется писать единый полиморфный код для объектов из таких иерархий в случае, когда эти объекты обладают схожим поведением. Как мы знаем, за поведение объектов отвечают методы. Это значит, что в полиморфном коде требуется для объектов из разных классов вызывать методы, имеющие одинаковую сигнатуру, но разную реализацию. Унарное наследование, которое мы изучали до сих пор, и при котором у класса может быть только один прародитель, не обеспечивает такой возможности. При унарном наследовании нельзя ввести переменную, которая бы могла ссылаться на экземпляры из разных иерархий, так как она должна иметь тип, совместимый с базовыми классами этих иерархий.

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

Декларация интерфейса очень похожа на декларацию класса:

Объявление константы осуществляется почти так же, как в классе:

Декларация метода в интерфейсе осуществляется очень похоже на декларацию абстрактного метода в классе – указывается только заголовок метода :

Пример задания интерфейса:

С помощью переменной типа интерфейс разрешается вызывать только методы, декларированные в данном интерфейсе, а не любые методы данного объекта. Если, конечно, не использовать приведение типа.

8.2. Отличия интерфейсов от классов. Проблемы наследования интерфейсов

Аналогичная ситуация может возникнуть и с константами, хотя, в отличие от методов, реально этого почти никогда не происходит.

Для использования констант из разных интерфейсов решением является квалификация имени константы именем соответствующего интерфейса – все аналогично разрешению конфликта имен в случае пакетов. Например:

Множественное наследование в Java может быть двух видов:

Ромбовидное наследование

magnify clip

Проблема ромба (англ. diamond problem ) получила свое название благодаря очертаниям диаграммы наследования классов в этой ситуации. В данной статье, класс A обозначается в виде вершины, классы B и C по отдельности указываются ниже, а D соединяется с обоими в самом низу, образуя ромб.

Содержание

Решения

Различные языки программирования решают проблему ромбовидного наследования следующими способами:

Прочие примеры

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Множественное наследование — это одна из ключевых особенностей языка C++. Рассмотрим, когда оно может потребоваться и как его использовать.

Назначение множественного наследования

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

Далее везде, где нужно рисование логотипа, вызываем функцию Logo.

Теперь нам нужно то же самое сделать в C++. Предположим, что у нас уже есть классы рисования квадрата и круга. Как нарисовать логотип?

Можно, конечно, сделать так:

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

Использование множественного наследования

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

Отличия множественного наследования

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


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

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


Ошибка неоднозначности

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

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

Алмаз смерти

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


В рассмотренном примере добавим суперкласс Shape.

Мы сразу получаем множество проблем неоднозначности вызова. Так, например, если мы видим в классе Logo координаты (x, y) , то какие именно координаты мы имеем в виду?

Проблемы множественного наследования в крупных проектах

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

Лучше всего эту картину иллюстрирует карточный домик.


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

Запрет наследования

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

В этот момент к вам подходит коллега и говорит:

— Слушай, я унаследовал твой класс и класс Угрюмова. И получил алмаз смерти. Иди и решай с Угрюмовым проблему неоднозначности.

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

— А может ты не будешь наследовать мой класс?!

Для подобных случаев в стандарт C++ 11 добавлен спецификатор final, который запрещает наследовать данный класс. Для этого нужно написать ключевое слово final после имени класса.

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