Ответы:
Множественное наследование (сокращенно MI) пахнет , что означает, что обычно это было сделано по плохим причинам, и это отразится на лице сопровождающего.
Это верно для наследования, и, таким образом, это еще более верно для множественного наследования.
Ваш объект действительно должен наследовать от другого? А Car
не нужно наследовать от, Engine
чтобы работать, ни от Wheel
. А Car
имеет Engine
и четыре Wheel
.
Если вы используете множественное наследование для решения этих проблем вместо композиции, то вы сделали что-то не так.
Обычно у вас есть класс A
, тогда B
и C
оба наследуются от A
. И (не спрашивайте меня почему), кто-то решает, что D
должен наследовать как от, так B
и от C
.
Я сталкивался с такой проблемой дважды за восемь восьмых лет, и это забавно видеть из-за:
D
не следовало наследовать от обоих B
и C
), потому что это была плохая архитектура (на самом деле, C
вообще не должно было существовать ...)A
присутствовал дважды в классе своего внука D
, и, таким образом, обновление одного родительского поля A::field
означало либо его обновление дважды (через B::field
и C::field
), либо что-то пошло не так и произвело сбой, позже (создать новый указатель B::field
и удалить C::field
...)Использование ключевого слова virtual в C ++ для определения наследования позволяет избежать двойной структуры, описанной выше, если это не то, что вам нужно, но в любом случае, по моему опыту, вы, вероятно, делаете что-то не так ...
В иерархии объектов вы должны попытаться сохранить иерархию в виде дерева (узел имеет ОДНОГО родителя), а не в виде графика.
Настоящая проблема с Diamond of Dread в C ++ ( при условии, что дизайн продуман - пересмотрите свой код! ) Заключается в том , что вам нужно сделать выбор :
A
существовал дважды в вашем макете, и что это значит? Если да, то непременно наследуй от него дважды.Этот выбор присущ этой проблеме, и в C ++, в отличие от других языков, вы можете сделать это, не догоняя ваш дизайн на уровне языка.
Но, как и все силы, с этой силой приходит ответственность: пересмотреть свой дизайн.
Многократное наследование нуля или одного конкретного класса и нуля или более интерфейсов обычно хорошо, потому что вы не столкнетесь с Diamond of Dread, описанным выше. На самом деле, именно так все и делается в Java.
Обычно, что вы имеете в виду, когда C наследует A
и B
что пользователи могут использовать, C
как если бы это было A
, и / или как если бы это было B
.
В C ++ интерфейс - это абстрактный класс, который имеет:
Множественное наследование от нуля до одного реального объекта и от нуля или более интерфейсов не считается "вонючим" (по крайней мере, не так много).
Во-первых, шаблон NVI можно использовать для создания интерфейса, потому что реальный критерий - отсутствие состояния (т.е. никаких переменных-членов, кроме this
). Цель вашего абстрактного интерфейса - опубликовать контракт («ты можешь называть меня так и так»), ни больше, ни меньше. Ограничение наличия только абстрактного виртуального метода должно быть выбором дизайна, а не обязательством.
Во-вторых, в C ++ имеет смысл наследовать виртуально от абстрактных интерфейсов (даже с дополнительными издержками / косвенностью). Если вы этого не сделаете, а наследование интерфейса появляется в вашей иерархии несколько раз, то у вас будут неоднозначности.
В- третьих, ориентация объекта является большим, но это не единственная истина Out Там TM в C ++. Используйте правильные инструменты и всегда помните, что у вас есть другие парадигмы в C ++, предлагающие различные виды решений.
Иногда да.
Обычно ваш 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
полностью, полностью, полностью НЕ ОТНОШЕНО ... Это резко снижает опасность множественного наследования)
Есть и другие случаи:
this
)Иногда вы можете использовать композицию, а иногда лучше. Дело в том, что у вас есть выбор. Сделайте это ответственно (и пересмотрите свой код).
Большую часть времени, по моему опыту, нет. MI - это не тот инструмент, даже если кажется, что он работает, потому что он может использоваться ленивым, чтобы складывать элементы вместе, не осознавая последствий (таких как создание как a, Car
так Engine
и a Wheel
).
Но иногда да. И в то время ничто не будет работать лучше, чем МИ.
Но поскольку MI вонючий, будьте готовы защищать свою архитектуру в обзорах кода (и защищать ее - это хорошо, потому что, если вы не можете защитить ее, вам не следует этого делать).
Из интервью с Бьярном Страуструпом :
Люди совершенно правильно говорят, что вам не нужно множественное наследование, потому что все, что вы можете сделать с множественным наследованием, вы можете сделать и с одиночным наследованием. Вы просто используете трюк делегирования, который я упомянул. Более того, вам вообще не нужно наследование, потому что все, что вы делаете с единичным наследованием, вы также можете сделать без наследования путем пересылки через класс. На самом деле, вам также не нужны никакие классы, потому что вы можете делать все это с помощью указателей и структур данных. Но почему вы хотите это сделать? Когда удобно пользоваться языковыми средствами? Когда бы вы предпочли обходной путь? Я видел случаи, когда множественное наследование полезно, и я даже видел случаи, когда полезно довольно сложное множественное наследование. Обычно я предпочитаю использовать средства, предлагаемые языком, для обхода
const
- мне пришлось писать неуклюжие обходные пути (обычно с использованием интерфейсов и композиции), когда класс действительно должен иметь изменяемые и неизменяемые варианты. Однако я ни разу не пропустил множественное наследование и никогда не чувствовал, что мне пришлось написать обходной путь из-за отсутствия этой функции. В этом разница. В любом случае , если я когда - либо видел, не используя MI это лучший выбор дизайна, а не обходной путь.
Нет причин избегать этого, и это может быть очень полезно в ситуациях. Вы должны знать о потенциальных проблемах, хотя.
Самым большим из них является алмаз смерти:
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;
Всегда проверяйте свой дизайн, убедитесь, что вы не используете наследование, чтобы сэкономить на повторном использовании данных. Если вы можете представлять то же самое с помощью композиции (и, как правило, можете), это гораздо лучший подход.
GrandParent
в Child
. Существует страх перед МИ, потому что люди просто думают, что могут не понимать языковые правила. Но любой, кто не может получить эти простые правила, также не может написать нетривиальную программу.
Смотрите w: множественное наследование .
Множественное наследование подверглось критике и, как таковое, не реализовано во многих языках. Критика включает в себя:
- Повышенная сложность
- Семантическая неоднозначность часто сводится к проблеме алмазов .
- Невозможность явно наследовать несколько раз от одного класса
- Порядок наследования с изменением семантики класса.
Многократное наследование в языках с конструкторами в стиле C ++ / Java усугубляет проблему наследования конструкторов и связывания конструкторов, создавая тем самым проблемы обслуживания и расширяемости в этих языках. Объекты в отношениях наследования с сильно различающимися методами построения трудно реализовать в рамках парадигмы связывания конструктора.
Современный способ решения этой проблемы - использовать интерфейс (чистый абстрактный класс), такой как интерфейс COM и Java.
Я могу сделать другие вещи вместо этого?
Да, ты можешь. Я собираюсь украсть у GoF .
Публичное наследование - это отношения IS-A, и иногда класс будет типом нескольких разных классов, и иногда важно отражать это.
"Mixins" также иногда полезны. Как правило, это небольшие классы, обычно не наследующие от чего-либо, обеспечивающие полезную функциональность.
Пока иерархия наследования является довольно мелкой (как и должно быть почти всегда) и хорошо управляемой, вы вряд ли получите ужасное наследование алмазов. Алмаз не является проблемой для всех языков, которые используют множественное наследование, но обработка C ++ его часто неуклюжа и иногда озадачивает.
Хотя я сталкивался со случаями, когда множественное наследование очень удобно, на самом деле они довольно редки. Это вероятно потому, что я предпочитаю использовать другие методы проектирования, когда мне действительно не нужно множественное наследование. Я предпочитаю избегать путаницы языковых конструкций, и легко создавать случаи наследования, когда вам нужно очень хорошо прочитать руководство, чтобы выяснить, что происходит.
Вам не следует «избегать» множественного наследования, но вы должны знать о проблемах, которые могут возникнуть, таких как «проблема с бриллиантами» ( http://en.wikipedia.org/wiki/Diamond_problem ), и относиться к власти, предоставленной вам, с осторожностью. , как вы должны со всеми силами.
Рискуя стать немного абстрактным, я нахожу интересным думать о наследовании в рамках теории категорий.
Если мы думаем обо всех наших классах и стрелках между ними, обозначающих наследственные отношения, то что-то вроде этого
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
наследуемый от B
and C
, то D
это класс, который R
можно переписать как подкласс D
.
Убедитесь, что ваши кончики алмаза являются откатами и отжимами, дает нам хороший способ для общего решения проблем с именами или обслуживания, которые могут возникнуть в противном случае.
Примечание Paercebal «s ответ вдохновил это как его увещевания вытекает из приведенной выше модели , учитывая , что мы работаем в полной категории класса всех возможных классов.
Я хотел обобщить его аргумент до чего-то, что показывает, насколько сложные множественные наследственные отношения могут быть как мощными, так и не проблемными.
TL; DR. Думайте об отношениях наследования в вашей программе как о формировании категории. Тогда вы сможете избежать проблем с Diamond of Doom, сделав множественные наследуемые классы push-и симметрично, сделав общий родительский класс, который является откатом.
Мы используем Eiffel. У нас отличный МИ. Не беспокойся. Без вопросов. Легко управляемый. Есть времена, чтобы НЕ использовать МИ. Тем не менее, это полезно больше, чем люди понимают, потому что они: А) на опасном языке, который плохо справляется с этим -ИЛИ- Б) удовлетворены тем, как они обходились с МИ годами и годами -ИЛИ- С) другими причинами ( слишком много, чтобы перечислить, я вполне уверен - см. ответы выше).
Для нас, используя Eiffel, MI такой же естественный, как и все остальное, и еще один прекрасный инструмент в наборе инструментов. Честно говоря, мы совершенно не обеспокоены тем, что никто другой не использует Eiffel. Не беспокойся. Мы довольны тем, что имеем, и приглашаем вас посмотреть.
Пока вы смотрите: обратите особое внимание на Void-безопасность и устранение разыменования нулевого указателя. Пока мы все танцуем вокруг MI, ваши указатели теряются! :-)
У каждого языка программирования есть немного различное отношение к объектно-ориентированному программированию с за и против. В версии 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» здесь не то же самое, что класс параметризованного шаблона (см. Эту ссылку для хорошего объяснения), но я думаю, что это справедливое использование терминологии.
Теперь я не хочу создавать впечатление, что это единственный способ безопасно использовать множественное наследование. Это просто один из способов, который довольно легко проверить.
Вы должны использовать это осторожно, есть некоторые случаи, например, проблема с алмазами , когда все может сложиться.
(источник: learncpp.com )
Printer
даже не должно быть PoweredDevice
. А Printer
для печати, а не управления питанием. Реализация конкретного принтера может потребовать некоторого управления питанием, но эти команды управления питанием не должны предоставляться непосредственно пользователям принтера. Я не могу представить себе использование этой иерархии в реальном мире.
Использование и злоупотребления наследования.
Статья делает большую работу по объяснению наследования, и это опасно.
Помимо ромбовидного рисунка множественное наследование усложняет понимание объектной модели, что, в свою очередь, увеличивает затраты на обслуживание.
Композиция по своей сути проста для понимания, понимания и объяснения. Написание кода для него может быть утомительным, но хорошая 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?
Уххх, конечно, вы получаете ошибку компиляции (если использование неоднозначно).
Ключевая проблема с MI конкретных объектов заключается в том, что редко у вас есть объект, который на законных основаниях должен быть «быть A и быть B», поэтому это редко является правильным решением по логическим причинам. Гораздо чаще у вас есть объект C, который подчиняется «C может действовать как A или B», чего вы можете достичь с помощью наследования и компоновки интерфейса. Но не заблуждайтесь - наследование нескольких интерфейсов все еще является MI, только его подмножеством.
В частности, для C ++ ключевым недостатком функции является не фактическое СУЩЕСТВОВАНИЕ множественного наследования, но некоторые конструкции, которые допускают это, почти всегда имеют дефекты. Например, наследование нескольких копий одного и того же объекта, например:
class B : public A, public A {};
НЕПРАВИЛЬНО ОПРЕДЕЛЕН. В переводе на английский это «B - это A и A». Так что даже в человеческом языке есть серьезная двусмысленность. Вы имели в виду "B имеет 2 As" или просто "B есть A"? Допуская такой патологический код и, что еще хуже, сделав его примером использования, С ++ не оказал никакой пользы, когда дело дошло до того, что он сохранил эту функцию на последующих языках.
Вы можете использовать композицию в предпочтении наследования.
Общее ощущение, что композиция лучше, и это очень хорошо обсуждается.
The general feeling is that composition is better, and it's very well discussed.
Это не значит, что композиция лучше.
требуется 4/8 байт на каждый участвующий класс. (Один этот указатель на класс).
Это никогда не должно вызывать беспокойства, но если однажды у вас будет структура микроданных, которая будет создаваться миллиарды раз, это будет происходить.