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

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

Ответ 1

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

Резюме

  • Рассмотрим состав функций, а не наследование
  • Будьте осторожны с Алмазом Страха.
  • Рассмотрим наследование нескольких интерфейсов вместо объектов
  • Иногда, множественное наследование - это правильная вещь. Если это так, используйте его.
  • Будьте готовы защитить свою многонаправленную архитектуру в обзорах кода.

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

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

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

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

2. Алмаз страха

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

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

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

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

В иерархии объектов вам следует попытаться сохранить иерархию как дерево (a node имеет один родительский элемент), а не как график.

Подробнее о Diamond (редактировать 2017-05-03)

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

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

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

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

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

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

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

В С++ интерфейс представляет собой абстрактный класс, который имеет:

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

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

Подробнее о абстрактном интерфейсе С++ (редактировать 2017-05-03)

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

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

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

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

Иногда, да.

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

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

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

Итак, у вас есть свой own::Node вывод из ai::Agent и geo::Point.

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

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

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

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

Существуют и другие случаи:

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

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

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

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

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

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

Ответ 2

Из интервью

Ответ 3

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

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

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

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

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

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

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

Ответ 4

См. w: Несколько наследований.

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

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

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

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

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

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

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

Ответ 5

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

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

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

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

Ответ 6

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

Ответ 8

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

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

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

Теперь проблемы, связанные с Diamond of Death здесь, когда 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, сделав многонаследованные классы pushouts и симметрично, создав общий родительский класс, который является откатом.

Ответ 9

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

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

Как это часто бывает в С++, если следовать базовому руководству, вы можете использовать его безопасно, не "постоянно переглядывая свое плечо". Основная идея заключается в том, что вы различаете особый вид определения класса, называемый "mix-in"; class является смешанным, если все его функции-члены являются виртуальными (или чистыми виртуальными). Тогда вам разрешено наследовать от одного основного класса и столько "микширования", сколько вам нравится, но вы должны наследовать mixins с ключевым словом "virtual". например.

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

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

Ответ 11

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

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

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

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

Ответ 12

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

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

class B : public A, public A {};

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

Ответ 13

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

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

Ответ 14

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

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

Ответ 15

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

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

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