Почему я должен избегать множественного наследования в C ++?

167

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

Хай
источник

Ответы:

259

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

Резюме

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

1. Может быть, композиция?

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

Ваш объект действительно должен наследовать от другого? А Carне нужно наследовать от, Engineчтобы работать, ни от Wheel. А 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 ++ имеет смысл наследовать виртуально от абстрактных интерфейсов (даже с дополнительными издержками / косвенностью). Если вы этого не сделаете, а наследование интерфейса появляется в вашей иерархии несколько раз, то у вас будут неоднозначности.

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

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

Иногда да.

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

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

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

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

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

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

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

Другие случаи (редактировать 2017-05-03)

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

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

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

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

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

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

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

paercebal
источник
4
Я бы сказал, что в этом нет необходимости, и он настолько редко полезен, что, даже если он идеально подходит, экономия на делегировании не компенсирует путаницу с программистами, не привыкшими ее использовать, но в остальном хорошее резюме.
Билл К
2
Я хотел бы добавить к пункту 5, что код, который использует MI, должен иметь рядом с ним комментарий, объясняющий причину, поэтому вам не нужно объяснять это в устной форме в обзоре кода. Без этого кто-то, кто не является вашим рецензентом, может поставить под сомнение ваше здравое суждение, когда увидит ваш код, и у вас может не быть возможности защитить его.
Тим Абелл
« потому что вы не встретите Алмаз Ужаса, описанный выше. » Почему бы и нет?
любопытный парень
1
Я использую МИ для наблюдателя.
Кальмарий
13
@BillK: Конечно, это не обязательно. Если у вас есть полный язык Turning без MI, вы можете делать все, что может сделать язык с MI. Так что да, это не обязательно. Когда-либо. Тем не менее, это может быть очень полезным инструментом, и так называемый Dreaded Diamond ... Я так и не понял, почему слово "dreaded" присутствует даже там. Это на самом деле довольно легко рассуждать и, как правило, не проблема.
Томас Эдинг
145

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

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

Неманья Трифунович
источник
6
Есть некоторые особенности C ++, которые я пропустил в C # и Java, хотя их использование сделало бы дизайн более сложным / сложным. Например, гарантия неизменности const- мне пришлось писать неуклюжие обходные пути (обычно с использованием интерфейсов и композиции), когда класс действительно должен иметь изменяемые и неизменяемые варианты. Однако я ни разу не пропустил множественное наследование и никогда не чувствовал, что мне пришлось написать обходной путь из-за отсутствия этой функции. В этом разница. В любом случае , если я когда - либо видел, не используя MI это лучший выбор дизайна, а не обходной путь.
BlueRaja - Дэнни Пфлугхофт
24
+1: Идеальный ответ: Если вы не хотите его использовать, не используйте его.
Томас Эдинг
38

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

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

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;

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

BillyBob
источник
21
А если запретить наследование и использовать только состав вместо этого, у вас всегда есть два GrandParentв Child. Существует страх перед МИ, потому что люди просто думают, что могут не понимать языковые правила. Но любой, кто не может получить эти простые правила, также не может написать нетривиальную программу.
любопытный парень
1
Это звучит грубо, правила C ++ повсюду очень сложны. Итак, даже если отдельные правила просты, их много, что делает все это непростым, по крайней мере, для человека, которому нужно следовать.
Данио
11

Смотрите w: множественное наследование .

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

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

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

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

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

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

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

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

"Mixins" также иногда полезны. Как правило, это небольшие классы, обычно не наследующие от чего-либо, обеспечивающие полезную функциональность.

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

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

Дэвид Торнли
источник
6

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

jwpfox
источник
1
+1: так же, как вы не должны избегать указателей; вам просто нужно знать об их последствиях. Люди должны прекратить греметь на фразы (такие как «MI - зло», «не оптимизировать», «goto is evil»), которые они выучили в интернете только потому, что некоторые хиппи сказали, что это плохо для их гигиены. Хуже всего то, что они никогда даже не пытались использовать такие вещи, а просто воспринимают это так, как будто они говорят из Библии.
Томас Эдинг
1
Я согласен. Не «избегайте» этого, но знайте лучший способ справиться с проблемой. Возможно, я бы предпочел использовать композиционные черты, но в C ++ этого нет. Возможно, я бы предпочел использовать автоматическое делегирование «ручек», но в C ++ этого нет. Поэтому я использую общий и тупой инструмент МИ для нескольких разных целей, возможно, одновременно.
JDługosz
3

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

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

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то, что называется выталкиванием .

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

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

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

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

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

Ncat
источник
3

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

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

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

Larry
источник
2

У каждого языка программирования есть немного различное отношение к объектно-ориентированному программированию с за и против. В версии 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» здесь не то же самое, что класс параметризованного шаблона (см. Эту ссылку для хорошего объяснения), но я думаю, что это справедливое использование терминологии.

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

sfkleach
источник
2

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

альтернативный текст
(источник: learncpp.com )

CMS
источник
12
Это плохой пример, потому что копир должен состоять из сканера и принтера, а не наследоваться от них.
Сванте
2
Во всяком случае, Printerдаже не должно быть PoweredDevice. А Printerдля печати, а не управления питанием. Реализация конкретного принтера может потребовать некоторого управления питанием, но эти команды управления питанием не должны предоставляться непосредственно пользователям принтера. Я не могу представить себе использование этой иерархии в реальном мире.
любопытный парень
1

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

Композиция по своей сути проста для понимания, понимания и объяснения. Написание кода для него может быть утомительным, но хорошая IDE (прошло уже несколько лет с тех пор, как я работал с Visual Studio, но, безусловно, все IDE Java имеют отличные инструменты автоматизации сочетаний клавиш) должны преодолеть это препятствие.

Кроме того, с точки зрения обслуживания, «проблема с бриллиантами» возникает и в случаях не буквального наследования. Например, если у вас есть A и B, и ваш класс C расширяет их оба, а у A есть метод 'makeJuice', который делает апельсиновый сок, а вы расширяете его, чтобы сделать апельсиновый сок с оттенком лайма: что происходит, когда дизайнер для ' B 'добавляет метод' makeJuice ', который генерирует и электрический ток? «А» и «В» могут быть совместимыми «родителями» прямо сейчас , но это не значит, что они всегда будут такими!

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

Том Диббл
источник
what happens when the designer for 'B' adds a 'makeJuice' method which generates and electrical current?Уххх, конечно, вы получаете ошибку компиляции (если использование неоднозначно).
Томас Эдинг
1

Ключевая проблема с MI конкретных объектов заключается в том, что редко у вас есть объект, который на законных основаниях должен быть «быть A и быть B», поэтому это редко является правильным решением по логическим причинам. Гораздо чаще у вас есть объект C, который подчиняется «C может действовать как A или B», чего вы можете достичь с помощью наследования и компоновки интерфейса. Но не заблуждайтесь - наследование нескольких интерфейсов все еще является MI, только его подмножеством.

В частности, для C ++ ключевым недостатком функции является не фактическое СУЩЕСТВОВАНИЕ множественного наследования, но некоторые конструкции, которые допускают это, почти всегда имеют дефекты. Например, наследование нескольких копий одного и того же объекта, например:

class B : public A, public A {};

НЕПРАВИЛЬНО ОПРЕДЕЛЕН. В переводе на английский это «B - это A и A». Так что даже в человеческом языке есть серьезная двусмысленность. Вы имели в виду "B имеет 2 As" или просто "B есть A"? Допуская такой патологический код и, что еще хуже, сделав его примером использования, С ++ не оказал никакой пользы, когда дело дошло до того, что он сохранил эту функцию на последующих языках.

Зак Йезек
источник
0

Вы можете использовать композицию в предпочтении наследования.

Общее ощущение, что композиция лучше, и это очень хорошо обсуждается.

Али Афшар
источник
4
-1: The general feeling is that composition is better, and it's very well discussed.Это не значит, что композиция лучше.
Томас Эдинг
Кроме того, на самом деле, лучше.
Али Афшар
12
За исключением случаев, когда это не лучше.
Томас Эдинг
0

требуется 4/8 байт на каждый участвующий класс. (Один этот указатель на класс).

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

Ронни Брендель
источник
1
Уверены в этом? Как правило, любое серьезное наследование требует указателя в каждом члене класса, но обязательно ли оно масштабируется с наследованными классами? Более того, сколько раз у вас были миллиарды маленьких структур данных?
Дэвид Торнли
3
@DavidThornley « Сколько раз у вас были миллиарды маленьких структур данных? » Когда вы выдвигаете аргументы против МИ.
любопытный парень