Почему я должен избегать множественного наследования в C++?
Это хорошая концепция, чтобы использовать множественное наследование или я могу делать другие вещи вместо этого?
15 ответов:
множественное наследование (сокращенно MI) пахнет, что означает обычно, это было сделано по плохим причинам, и он будет дуть обратно в лицо сопровождающего.
резюме
- рассмотрим состав признаков, а не наследование
- будьте осторожны с Алмазом страха
- рассмотрим наследование нескольких интерфейсов вместо объектов
- Иногда, Множественное Наследование это правильно. Если это так, то используйте его.
- будьте готовы защищать свою множественную унаследованную архитектуру в обзорах кода
1. Может быть, композиция?
это верно для наследования, и поэтому, это еще более верно для множественного наследования.
действительно ли ваш объект должен наследовать от другого? А
Car
не нужно наследовать отEngine
работать, ни от АWheel
. АCar
естьEngine
и четыреWheel
.если вы используете множественное наследование для решения этих проблем вместо композиции, то вы сделали что-то неправильно.
2. Алмаз страха
обычно, у вас есть класс
A
, потомB
иC
оба наследуют отA
. И (не спрашивайте меня, почему) кто-то тогда решает, чтоD
должны наследовать отB
иC
.я сталкивался с такой проблемой дважды за 8 лет, и это забавно видеть из-за:
- как много ошибок было с самого начала (в обоих случаях
D
не должно было наследоваться от обоихB
иC
), потому что это была плохая архитектура (на самом деле,C
не должно существовать вообще...)- сколько сопровождающие платили за это, потому что в C++ родительский класс
A
присутствовал дважды в своем классе внукD
, и таким образом, обновление одного родительского поляA::field
означает либо обновление его дважды (черезB::field
иC::field
), или что-то идет тихо неправильно и сбой, позже (новый указатель вB::field
и удалитьC::field
...)использование ключевого слова virtual в C++ для квалификации наследования позволяет избежать двойного макета, описанного выше, если это не то, что вы хотите, но в любом случае, по моему опыту, вы, вероятно, делаете что-то неправильно...
в иерархии объектов, вы должны попытаться сохранить иерархию в виде дерева (узел один родитель), а не как график.
подробнее о бриллианте (edit 2017-05-03)
реальная проблема с Алмазом страха в C++ (предполагая, что дизайн является звуковым - ваш код рассмотрен!), что вам нужно сделать выбор:
- это желательно для класса
A
существовать дважды в вашем макете, и что это значит? Если да, то непременно наследуйте от него дважды.- если он должен существовать только один раз, а потом унаследовать от него практически.
этот выбор присущ проблеме, и в C++, в отличие от других языков, вы можете фактически сделать это без догмы, заставляющей ваш дизайн на уровне языка.
но, как и все полномочия, с этой властью приходит ответственность: ваш дизайн рассматривается.
3. Интерфейсы
множественное наследование нуля или одного конкретного класса, а также ноль или более интерфейсов обычно в порядке, потому что вы не будете столкнитесь с Алмазом страха, описанным выше. На самом деле, это как делается в Java.
обычно, что вы имеете в виду, когда C наследует от
A
иB
это то, что пользователи могут использоватьC
какA
, и / или как будто это былоB
.в C++, интерфейс-это абстрактный класс, который имеет:
весь его метод объявлен чисто виртуальным (с суффиксом = 0)(удалена 2017-05-03)- нет переменных-членов
множественное наследование нуля до одного реального объекта, а также ноль или более интерфейсов не считается "вонючим" (по крайней мере, не так много).
подробнее об абстрактном интерфейсе C++ (edit 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
полностью, полностью, полностью не связаны... Это резко снижает опасность множественного наследования)другие случаи (edit 2017-05-03)
Есть и другие случаи:
- использование (надеюсь, частного) наследования в качестве детали реализации
- некоторые идиомы C++, такие как политики, могут использовать множественное наследование (когда каждая часть нуждается общаться с другими через
this
)- виртуальное наследование от std:: exception (необходимо ли виртуальное наследование для исключений?)
- etc.
иногда вы можете использовать композицию, а иногда MI лучше. Смысл в том, что у вас есть выбор. Сделайте это ответственно (и проверьте свой код).
5. Итак, я должен сделать множественное наследование?
большую часть времени, по моему опыту, нет. MI не является правильным инструментом, даже если он, кажется, работает, потому что он может быть использован ленивым, чтобы сложить функции вместе, не осознавая последствий (например, сделать
Car
иEngine
иWheel
).но иногда, да. И в то время, ничто не будет работать лучше, чем Ми.
но поскольку MI воняет, будьте готовы защищать свою архитектуру в обзорах кода (и защищать это хорошо, потому что если вы не можете ее защитить, то вам не следует этого делать он.)
с интервью с Бьярне Страуструп:
люди совершенно правильно говорят, что вам не нужно множественное наследование, потому что все, что вы можете сделать с множественным наследованием, вы также можете сделать с одним наследованием. Вы просто используете трюк делегирования, о котором я упоминал. Кроме того, вам вообще не нужно наследование, потому что все, что вы делаете с одним наследованием, вы также можете сделать без наследования, переадресовав через класс. На самом деле, вам не нужно любые занятия, потому что вы можете делать все это с указателями и структурами данных. Но зачем тебе это делать? Когда удобно пользоваться языковыми средствами? Когда бы вы предпочли обходной путь? Я видел случаи, когда множественное наследование полезно, и я даже видел случаи, когда довольно сложное множественное наследование полезно. Как правило, я предпочитаю использовать возможности, предлагаемые языком, чтобы делать обходные пути
нет никаких причин, чтобы ее избежать, и это может быть очень полезно в ситуациях. Вы должны быть в курсе потенциальных проблем, хотя.
самый большой из них-Алмаз смерти:
class GrandParent; class Parent1 : public GrandParent; class Parent2 : public GrandParent; class Child : public Parent1, public Parent2;
теперь у вас есть две "копии" бабушки и дедушки внутри ребенка.
C++ подумал об этом, хотя и позволяет делать виртуальное наследование, чтобы обойти проблемы.
class GrandParent; class Parent1 : public virtual GrandParent; class Parent2 : public virtual GrandParent; class Child : public Parent1, public Parent2;
всегда просматривайте свой дизайн, убедитесь, что вы не используете наследование для сохранения данных повторное использование. Если вы можете представить то же самое с композицией (и обычно вы можете), это гораздо лучший подход.
см. w:Множественное Наследование.
получено множественное наследование критики и как таковой нет реализовано на многих языках. Критика включает в себя:
- повышенной сложности
- семантическая двусмысленность часто суммируется как Алмаз проблема.
- не будучи в состоянии явно наследовать несколько раз из одного класс
- порядок наследования изменение класса семантика.
множественное наследование в языке с Конструкторы стиля C++ / Java усугубляет проблему наследования конструкторы и цепочка конструкторов, таким образом создавая обслуживание и проблемы расширяемости в этих языки. Объекты в порядке наследования отношения с сильно варьирующимися методы строительства трудно реализовать под конструктором цепная парадигма.
современный способ решения этой проблемы использовать интерфейс (чистый абстрактный класс), как интерфейс COM и Java.
Я могу делать другие вещи, вместо этого?
да, можно. Я собираюсь украсть у г.
- программа для интерфейса, а не реализация
- предпочитайте композицию наследованию
публичное наследование-это отношение IS-a, и иногда класс будет типом нескольких разных классов,и иногда это важно отразить.
"миксины" также иногда полезны. Они, как правило, небольшие классы, обычно не наследуя ни от чего, обеспечивая полезную функциональность.
пока иерархия наследования довольно мелкая (как это должно быть почти всегда) и хорошо управляется, вы вряд ли получите страшный Алмаз наследование. Алмаз не является проблемой для всех языков, которые используют множественное наследование, но обработка C++часто неудобна и иногда озадачивает.
хотя я сталкивался со случаями, когда множественное наследование очень удобно, они на самом деле довольно редки. Вероятно, это связано с тем, что я предпочитаю использовать другие методы проектирования, когда мне действительно не нужно множественное наследование. Я предпочитаю избегать путаницы языковых конструкций, и легко построить случаи наследования, когда вы нужно очень хорошо прочитать инструкцию, чтобы понять, что происходит.
вы не должны "избегать" множественного наследования, но вы должны знать о проблемах, которые могут возникнуть, таких как "алмазная проблема" ( http://en.wikipedia.org/wiki/Diamond_problem) и относитесь к данной вам силе с осторожностью, как вы должны относиться ко всем силам.
вы должны использовать его осторожно, есть некоторые случаи, как Проблема, когда все может пойти сложнее.
alt текст http://www.learncpp.com/images/CppTutorial/Section11/PoweredDevice.gif
каждый язык программирования имеет несколько иной подход к объектно-ориентированному программированию с плюсами и минусами. Версия C++делает акцент прямо на производительности и имеет сопутствующий недостаток, что тревожно легко писать недопустимый код - и это верно для множественного наследования. Как следствие, существует тенденция уводить программистов от этой функции.
другие люди обращались к вопросу о том, что множественное наследование не хорошо. Но мы видели довольно много комментариев, которые более или менее подразумевают, что причина избежать этого заключается в том, что это небезопасно. Ну и да, и нет.
Как это часто бывает в C++, если вы следуете базовому руководству, вы можете использовать его безопасно, без необходимости постоянно "оглядываться через плечо". Ключевая идея заключается в том, что вы различаете особый вид определения класса, называемый "mix-in"; класс является mix-in, если все его функции-члены являются виртуальными (или чисто виртуальными). Тогда вам разрешено наследовать от одного основной класс и столько "mix-ins", как вам нравится - но вы должны наследовать 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" здесь не совпадает с параметризованным классом шаблона (см. этой ссылке для хорошего объяснения), но я думаю, что это справедливое использование терминологии.
теперь я не хочу создать впечатление, что это единственный способ безопасно использовать множественное наследование. Это просто один из способов, который довольно легко проверить.
рискуя стать немного абстрактным, я считаю, что полезно подумать о наследовании в рамках теории категорий.
если мы подумаем обо всех наших классах и стрелках между ними, обозначающих отношения наследования, то что-то вроде этого
A --> B
означает, что
class B
происходит отclass A
. Обратите внимание, что, учитываяA --> B, B --> C
мы говорим с производным от B, который наследуется от A, так тоже говорят, происходит от, таким образом
A --> C
кроме того, мы говорим, что для каждого класса
A
что тривиальноA
происходит отA
, таким образом, наша модель наследования выполняет определение категории. В более традиционном языке, у нас есть категорияClass
С объектами всех классов и морфизмов отношения наследования.это немного настройки, но с этим давайте взглянем на наш Алмаз судьбы:
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
называют откат. Это по существу самый общий полезный класс, который вы можете построить, который наследует от обоихB
иC
. То есть, если вы есть любой другой классR
умножить унаследованный отB
иC
, потомD
- это класс, гдеR
может быть переписан как подклассD
.убедившись, что ваши кончики алмаза являются откатами и выталкиваниями, дает нам хороший способ в целом обрабатывать проблемы с именем или проблемами обслуживания, которые могут возникнуть в противном случае.
ПримечаниеPaercebal ' s ответ вдохновил это, как его увещевания являются подразумевается приведенная выше модель, учитывая, что мы работаем в полном классе категорий всех возможных классов.
я хотел обобщить его аргумент на то, что показывает, насколько сложные отношения множественного наследования могут быть как мощными, так и не проблематичными.
TL; DR считайте, что отношения наследования в вашей программе образуют категорию. Тогда вы можете избежать проблем с Алмазом Doom, сделав многократно унаследованные классы pushouts и симметрично, делая общий родительский класс, который является откатом.
мы используем Eiffel. У нас отличный Ми. Не волнуйся. Никакой вопрос. Легко управлять. Есть время, чтобы не использовать MI. Тем не менее, это полезно больше, чем люди понимают, потому что они: а) на опасном языке, который не справляется с этим хорошо-или - Б) удовлетворены тем, как они работали вокруг ми в течение многих лет и лет-или - в) другие причины (слишком много, чтобы перечислить я совершенно уверен-см. ответы выше).
для нас, используя Eiffel, MI так же естественно, как и все остальное, и еще один прекрасный инструмент в наборе инструментов. Честно говоря, нас совершенно не волнует, что никто больше не использует Eiffel. Не волнуйся. Мы довольны тем, что у нас есть, и приглашаем вас посмотреть.
пока вы смотрите: обратите особое внимание на Void-безопасность и искоренение разыменования нулевого указателя. Пока мы все танцуем вокруг Ми, ваши указатели теряются! : -)
использование и злоупотребление наследования.
статья делает большую работу по объяснению наследования, и это опасности.
помимо ромбовидного узора, множественное наследование, как правило, затрудняет понимание объектной модели, что, в свою очередь, увеличивает затраты на обслуживание.
композиция по своей сути легко понять, понять и объяснить. Это может стать утомительным для написания кода, но хорошая IDE (прошло несколько лет с тех пор, как я работал с Visual Studio, но, конечно же, у всех IDE Java есть отличные инструменты автоматизации быстрого доступа к композиции) должна помочь вам преодолеть это препятствие.
также, в плане обслуживания, "проблема бриллианта" приходит в буквальном случаях наследования, а также. Например, если у вас есть A и B, и ваш класс C расширяет их оба, а у A есть метод "makeJuice", который делает апельсиновый сок, и вы расширяете его, чтобы сделать апельсиновый сок с завихрением Лайма: что происходит, когда дизайнер для " B "добавляет метод "makeJuice", который генерирует и электрический ток? 'A' и ' B 'могут быть совместимыми "родителями"прямо сейчас, но это не значит, что они будут всегда так будет!
в целом, Максима стремления избежать наследования, и особенно множественного наследования, является здравым. Как и все Максимы, есть исключения, но вам нужно убедиться, что есть мигающий зеленый неоновый знак, указывающий на любые исключения, которые вы кодируете (и тренируйте свой мозг, чтобы каждый раз, когда вы видите такие деревья наследования, вы рисуете свой собственный мигающий зеленый неоновый знак), и что вы проверяете, чтобы убедиться, что все это имеет смысл каждый раз в то время.
для C++ в частности, ключевая слабость функции это не фактическое существование множественного наследования, но некоторые конструкции, которые он позволяет, почти всегда искажены. Например, наследование нескольких копий одного и того же объекта, например:
class B : public A, public A {};
является уродливым по определению. В переводе на английский это "B-это A и A". Таким образом, даже в человеческом языке есть серьезная двусмысленность. Вы имели в виду "Б-2" или просто "Б-а"?. Позволяя такой патологический код, и что еще хуже, делая его примером использования, C++ не помогал, когда дело доходило до создание аргумента для сохранения функции на последующих языках.