Почему я должен избегать множественного наследования в C ++? - PullRequest
157 голосов
/ 02 января 2009

Хорошо ли использовать множественное наследование или я могу вместо этого заняться другими вещами?

Ответы [ 15 ]

248 голосов
/ 02 января 2009

Множественное наследование (сокращенно MI) пахнет , что означает, что обычно , это было сделано по дурным причинам, и оно отразится на лице сопровождающего.

Резюме

  1. Рассмотрим состав признаков вместо наследования
  2. Остерегайся Алмаза Ужаса
  3. Рассмотрим наследование нескольких интерфейсов вместо объектов
  4. Иногда множественное наследование является правильным. Если это так, используйте его.
  5. Будьте готовы защищать вашу множественную наследуемую архитектуру в обзорах кода

1. Возможно композиция?

Это верно для наследования, и поэтому еще более верно для множественного наследования.

Действительно ли ваш объект должен наследоваться от другого? Car не нужно наследовать от Engine для работы или от Wheel. A Car имеет Engine и четыре Wheel.

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

2. Алмаз ужаса

Обычно у вас есть класс A, тогда B и C оба наследуются от A. И (не спрашивайте меня почему), кто-то тогда решает, что D должен наследовать от B и C.

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

  1. Сколько было ошибки с самого начала (* В обоих случаях D не должен был наследоваться от B и C), потому что это была плохая архитектура (на самом деле, C не должна существовал вообще ...)
  2. Сколько платят сопровождающие за это, потому что в C ++ родительский класс A присутствовал дважды в классе внуков D, и, таким образом, обновление одного родительского поля A::field означало либо его обновление дважды (через B::field и C::field), или что-то пошло не так и произойдет сбой, позже (новый указатель в B::field и удаление C::field ...)

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

В иерархии объектов вы должны попытаться сохранить иерархию в виде дерева (узел имеет ОДНОГО родителя), а не в виде графика.

Подробнее о бриллианте (изменить 2017-05-03)

Реальная проблема с Diamond of Dread в C ++ ( при условии, что дизайн продуман - проверьте ваш код! ), заключается в том, что вам нужно сделать выбор :

  • Желательно ли, чтобы класс A существовал дважды в вашем макете, и что это значит? Если да, то непременно наследуй от него дважды.
  • если он должен существовать только один раз, то наследовать от него виртуально.

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

Но, как и все силы, с этой силой приходит ответственность: пересмотреть свой дизайн.

3. Интерфейсы

Множественное наследование нуля или одного конкретного класса и нуля или более интерфейсов обычно хорошо, потому что вы не столкнетесь с Diamond of Dread, описанным выше. На самом деле, именно так все и делается в Java.

Обычно, что вы имеете в виду, когда C наследует от A и B, что пользователи могут использовать C, как если бы это был A, и / или как если бы это был B.

В C ++ интерфейс - это абстрактный класс, который имеет:

  1. весь его метод объявлен чисто виртуальным (с добавлением = 0) (удалено 2017-05-03)
  2. нет переменных-членов

Множественное наследование от нуля до одного реального объекта и от нуля или более интерфейсов не считается "вонючим" (по крайней мере, не так много).

Подробнее об абстрактном интерфейсе C ++ (редакция 2017-05-03)

Во-первых, шаблон NVI можно использовать для создания интерфейса, поскольку реальный критерий - отсутствие состояния (т. Е. Никаких переменных-членов, кроме this). Цель вашего абстрактного интерфейса - опубликовать контракт («ты можешь называть меня так и так»), ни больше, ни меньше. Ограничение наличия только абстрактного виртуального метода должно быть выбором проекта, а не обязательством.

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

В-третьих, объектная ориентация великолепна, но это не Единственная правда, существующая там TM в C ++. Используйте правильные инструменты и всегда помните, что у вас есть другие парадигмы в C ++, предлагающие различные виды решений.

4. Вам действительно нужно множественное наследование?

Иногда да.

Обычно ваш класс C наследуется от A и B, а A и B являются двумя не связанными объектами (т.е. не в одной иерархии, ничего общего, разных концепций и т. Д.). ).

Например, у вас может быть система Nodes с координатами X, Y, Z, способная выполнять множество геометрических вычислений (возможно, точка, часть геометрических объектов), и каждый узел является Автоматизированным агентом, способным общаться с другими агентами.

Возможно, у вас уже есть доступ к двум библиотекам, каждая из которых имеет собственное пространство имен (еще одна причина использовать пространства имен ... Но вы используете пространства имен, не так ли?), Одна из которых geo, а другая ai

Таким образом, у вас есть собственный own::Node производный от ai::Agent и geo::Point.

Это момент, когда вы должны спросить себя, не следует ли вам вместо этого использовать композицию. Если own::Node действительно действительно ai::Agent и geo::Point, то композиция не подойдет.

Тогда вам потребуется множественное наследование, чтобы ваш own::Node мог общаться с другими агентами в соответствии с их положением в трехмерном пространстве.

(Вы заметите, что ai::Agent и geo::Point полностью, полностью, полностью НЕ СВЯЗАНЫ ... Это резко снижает опасность множественного наследования)

Другие случаи (изменить 2017-05-03)

Есть и другие случаи:

  • использование (возможно, частного) наследования в качестве детали реализации
  • некоторые идиомы C ++, такие как политики, могут использовать множественное наследование (когда каждая часть должна взаимодействовать с другими через this)
  • виртуальное наследование от std :: exception ( Требуется ли виртуальное наследование для исключений? )
  • и т.д.

Иногда вы можете использовать композицию, а иногда MI лучше. Дело в том, что у вас есть выбор. Сделайте это ответственно (и пересмотрите свой код).

5. Итак, я должен сделать множественное наследование?

Большую часть времени, по моему опыту, нет. MI - не тот инструмент, даже если кажется, что он работает, потому что он может быть использован ленивым для объединения функций без осознания последствий (например, создание Car и Engine и Wheel).

Но иногда да. И в то время ничто не будет работать лучше, чем МИ.

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

140 голосов
/ 02 января 2009

Из интервью с Бьярном Страуструпом :

Люди совершенно правильно говорят, что вам не нужно множественное наследование, потому что все, что вы можете сделать с множественным наследованием, вы можете сделать и с одиночным наследованием. Вы просто используете трюк делегирования, который я упомянул. Кроме того, вам вообще не нужно наследование, потому что все, что вы делаете с единичным наследованием, вы также можете сделать без наследования путем пересылки через класс. На самом деле, вам также не нужны никакие классы, потому что вы можете делать все это с помощью указателей и структур данных. Но почему вы хотите это сделать? Когда удобно пользоваться языковыми средствами? Когда бы вы предпочли обходной путь? Я видел случаи, когда множественное наследование полезно, и я даже видел случаи, когда довольно сложное множественное наследование полезно. Как правило, я предпочитаю использовать средства, предлагаемые языком, для обходных путей

38 голосов
/ 02 января 2009

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

Самым большим из них является алмаз смерти:

class GrandParent;
class Parent1 : public GrandParent;
class Parent2 : public GrandParent;
class Child : public Parent1, public Parent2;

Теперь у вас есть две «копии» GrandParent внутри Child.

C ++ подумал об этом и позволяет вам делать виртуальное наследование, чтобы обойти проблемы.

class GrandParent;
class Parent1 : public virtual GrandParent;
class Parent2 : public virtual GrandParent;
class Child : public Parent1, public Parent2;

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

11 голосов
/ 02 января 2009

См. W: Множественное наследование .

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

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

Множественное наследование в языках с Конструкторы в стиле C ++ / Java усугубляет проблему наследования конструкторы и конструкторские цепочки, тем самым создавая техническое обслуживание и проблемы растяжимости в этих языки. Объекты в наследстве отношения с сильно меняющимися методы строительства трудно реализовать под конструктор парадигма цепочки.

Современный способ решения этой проблемы - использовать интерфейс (чистый абстрактный класс), такой как интерфейс COM и Java.

Я могу делать другие вещи вместо этого?

Да, вы можете. Я собираюсь украсть у GoF .

  • Программа для интерфейса, а не реализация
  • Предпочитаю композицию наследованию
8 голосов
/ 02 января 2009

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

«Миксины» также иногда полезны. Обычно это небольшие классы, обычно не наследующие от чего-либо, обеспечивающие полезную функциональность.

Пока иерархия наследования является довольно поверхностной (как и должно быть почти всегда) и хорошо управляемой, вы вряд ли получите ужасное наследование алмазов. Алмаз не является проблемой для всех языков, которые используют множественное наследование, но обработка C ++ его часто неуклюжа и иногда озадачивает.

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

6 голосов
/ 02 января 2009

Вы не должны «избегать» множественного наследования, но вы должны знать о проблемах, которые могут возникнуть, таких как «проблема с алмазами» (http://en.wikipedia.org/wiki/Diamond_problem), и относиться к власти, предоставленной вам, с осторожностью, как вы со всеми силами.

2 голосов
/ 29 июня 2015

Мы используем Eiffel. У нас отличный МИ. Не волнуйтесь. Без вопросов. Легко управляемый. Есть моменты, чтобы НЕ использовать МИ. Тем не менее, это полезно больше, чем люди понимают, потому что они: А) на опасном языке, который плохо справляется с этим - ИЛИ - Б) удовлетворены тем, как они обходились с МИ годами и годами - ИЛИ В) другими причинами Слишком много, чтобы перечислять, я совершенно уверен - см. ответы выше).

Для нас, используя Eiffel, MI такой же естественный, как и все остальное, и еще один прекрасный инструмент в наборе инструментов. Честно говоря, мы совершенно не обеспокоены тем, что никто другой не использует Эйфелеву. Не волнуйтесь. Мы довольны тем, что имеем, и приглашаем вас посмотреть.

Пока вы смотрите: обратите особое внимание на безопасность пустот и устранение разыменования нулевого указателя. Пока мы все танцуем вокруг MI, ваши указатели теряются! : -)

2 голосов
/ 22 июля 2014

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

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

Как это часто бывает в C ++, если вы следуете основным правилам, вы можете безопасно их использовать, не оглядываясь через плечо. Ключевая идея заключается в том, что вы выделяете особый вид определения класса, называемый «смесью»; Класс является дополнением, если все его функции-члены являются виртуальными (или чисто виртуальными). Тогда вам разрешено наследовать от одного основного класса и столько «дополнений», сколько вам нужно - но вы должны наследовать смешивания с ключевым словом «виртуальный». например,

class CounterMixin {
    int count;
public:
    CounterMixin() : count( 0 ) {}
    virtual ~CounterMixin() {}
    virtual void increment() { count += 1; }
    virtual int getCount() { return count; }
};

class Foo : public Bar, virtual public CounterMixin { ..... };

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

Обратите внимание, что мое использование слова "mix-in" здесь не совпадает с параметризованным шаблоном класса (хорошее объяснение см. этой ссылкой ), но я думаю, что это справедливое использование терминология.

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

2 голосов
/ 13 февраля 2014

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

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

A --> B

означает, что class B происходит от class A. Обратите внимание, что, учитывая

A --> B, B --> C

мы говорим, что C происходит от B, который происходит от A, поэтому C также называется производным от A, таким образом

A --> C

Кроме того, мы говорим, что для каждого класса A, который тривиально A происходит от A, таким образом, наша модель наследования удовлетворяет определению категории. На более традиционном языке у нас есть категория Class с объектами всех классов и морфизмов отношения наследования.

Это немного подстроено, но давайте посмотрим на наш Diamond of Doom:

C --> D
^     ^
|     |
A --> B

Это теневая диаграмма, но она подойдет. Так что D наследует от всех A, B и C. Кроме того, и ближе к решению вопроса OP, D также наследуется от любого суперкласса A. Мы можем нарисовать диаграмму

C --> D --> R
^     ^
|     |
A --> B
^ 
|
Q

Теперь проблемы, связанные с Алмазом смерти, заключаются в том, что C и B разделяют некоторые имена свойств / методов, и вещи становятся неоднозначными; однако если мы перенесем какое-либо общее поведение в A, тогда неопределенность исчезнет.

Говоря категорично, мы хотим, чтобы A, B и C были такими, чтобы, если B и C наследовали от Q, тогда A можно было переписать как подкласс Q. Это делает A то, что называется pushout .

Существует также симметричная конструкция D, называемая pullback . По сути, это наиболее общий полезный класс, который вы можете создать, который наследуется от B и C. То есть, если у вас есть любой другой класс R, кратно наследуемый от B и C, то D - это класс, в котором R можно переписать как подкласс D.

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

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

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

TL; DR Думайте об отношениях наследования в вашей программе как о формировании категории. Тогда вы сможете избежать проблем с Diamond of Doom, выполнив множественные наследования и симметрично выполняя классы, создав общий родительский класс, который является откатом.

2 голосов
/ 02 января 2009

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

alt text
(источник: learncpp.com )

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...