Почему я должен избегать множественного наследования в С++?
Хорошо ли использовать множественное наследование или я могу вместо этого делать другие вещи?
Ответы
Ответ 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) и относиться к власти, предоставленной вам с осторожностью, как вам следует со всеми силами.
Ответ 7
Вы должны использовать его осторожно, есть некоторые случаи, например Diamond Problem, когда все может усложняться.
alt text http://www.learncpp.com/images/CppTutorial/Section11/PoweredDevice.gif
Ответ 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" здесь не совпадает с параметризованным классом шаблона (см. эта ссылка для хорошее объяснение), но я считаю, что это справедливое использование терминологии.
Теперь я не хочу создавать впечатление, что это единственный способ безопасно использовать множественное наследование. Это просто один из способов, который довольно легко проверить.
Ответ 10
Использование и злоупотребления наследования.
В статье проделана большая работа по объяснению наследования, и это опасно.
Ответ 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 и уничтожение разыменования нулевых указателей. Пока мы все танцуем вокруг МИ, ваши указатели теряются!: -)