Почему я должен избегать множественного наследования в C++?
Это хорошая идея, чтобы использовать множественное наследование, или я могу вместо этого делать другие вещи?
15 ответов
Множественное наследование (сокращенно MI) пахнет, что означает, что обычно это было сделано по плохим причинам, и это отразится на лице сопровождающего.
Резюме
- Рассмотрим состав признаков вместо наследования
- Остерегайтесь Алмаза Ужаса
- Рассмотрим наследование нескольких интерфейсов вместо объектов
- Иногда множественное наследование является правильным. Если это так, то используйте его.
- Будьте готовы защищать вашу множественную наследуемую архитектуру в обзорах кода
1. Может быть, композиция?
Это верно для наследования, и, таким образом, это еще более верно для множественного наследования.
Ваш объект действительно должен наследовать от другого? Car
не нужно наследовать от Engine
работать, ни от Wheel
, Car
имеет Engine
и четыре Wheel
,
Если вы используете множественное наследование для решения этих проблем вместо композиции, то вы сделали что-то не так.
2. Алмаз ужаса
Обычно у вас есть класс A
, затем B
а также C
оба наследуют от A
, И (не спрашивайте меня почему), кто-то решает, что D
должен наследовать как от B
а также C
,
Я сталкивался с такой проблемой дважды за восемь восьмых лет, и это забавно видеть из-за:
- Сколько ошибки это было с самого начала (В обоих случаях
D
не должен был унаследовать от обоихB
а такжеC
), потому что это была плохая архитектура (на самом деле,C
не должно было существовать вообще...) - Сколько платят за это сопровождающие, потому что в 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++ интерфейс - это абстрактный класс, который имеет:
-
все его методы объявлены чисто виртуальными (с суффиксом = 0)(удалено 2017-05-03) - нет переменных-членов
Множественное наследование от нуля до одного реального объекта и от нуля или более интерфейсов не считается "вонючим" (по крайней мере, не так много).
Подробнее об абстрактном интерфейсе C++ (редакция 2017-05-03)
Во-первых, шаблон NVI можно использовать для создания интерфейса, потому что реальный критерий - отсутствие состояния (т. Е. Никаких переменных-членов, кроме this
). Смысл вашего абстрактного интерфейса в том, чтобы опубликовать контракт ("ты можешь называть меня так и так"), ни больше, ни меньше. Ограничение наличия только абстрактного виртуального метода должно быть выбором дизайна, а не обязательством.
Во-вторых, в 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
полностью, полностью, полностью НЕ ОТНОШЕНЫ... Это резко снижает опасность множественного наследования)
Другие случаи (редактировать 2017-05-03)
Есть и другие случаи:
- использование (надеюсь частного) наследования в качестве детали реализации
- некоторые идиомы C++, такие как политики, могут использовать множественное наследование (когда каждая часть должна взаимодействовать с другими через
this
) - виртуальное наследование от std::exception ( Виртуальное наследование необходимо для исключений?)
- и т.п.
Иногда вы можете использовать композицию, а иногда лучше. Дело в том, что у вас есть выбор. Сделайте это ответственно (и пересмотрите свой код).
5. Итак, я должен сделать множественное наследование?
Большую часть времени, по моему опыту, нет. MI - это не тот инструмент, даже если кажется, что он работает, потому что он может использоваться ленивым для объединения функций без осознания последствий (например, создание Car
оба Engine
и Wheel
).
Но иногда да. И в то время ничто не будет работать лучше, чем МИ.
Но поскольку 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;
Всегда проверяйте свой дизайн, убедитесь, что вы не используете наследование, чтобы сэкономить на повторном использовании данных. Если вы можете представить то же самое с помощью композиции (и, как правило, можете), это гораздо лучший подход.
Смотрите w: множественное наследование.
Множественное наследование подверглось критике и, как таковое, не реализовано во многих языках. Критика включает в себя:
- Повышенная сложность
- Семантическая двусмысленность часто описывается как проблема алмазов.
- Невозможность явно наследовать несколько раз от одного класса
- Порядок наследования с изменением семантики класса.
Многократное наследование в языках с конструкторами в стиле C++/Java усугубляет проблему наследования конструкторов и связывания конструкторов, создавая тем самым проблемы обслуживания и расширяемости в этих языках. Объекты в отношениях наследования с сильно различающимися методами построения трудно реализовать в рамках парадигмы связывания конструктора.
Современный способ решения этой проблемы - использовать интерфейс (чистый абстрактный класс), такой как интерфейс COM и Java.
Я могу сделать другие вещи вместо этого?
Да, ты можешь. Я собираюсь украсть у GoF.
- Программа для интерфейса, а не реализация
- Предпочитаю композицию наследованию
Публичное наследование - это отношения IS-A, и иногда класс будет типом нескольких разных классов, и иногда важно отражать это.
"Mixins" также иногда полезны. Как правило, это небольшие классы, обычно не наследующие от чего-либо, обеспечивающие полезную функциональность.
Пока иерархия наследования является довольно мелкой (как это должно быть почти всегда) и хорошо управляемой, вы вряд ли получите ужасное наследование алмазов. Алмаз не является проблемой для всех языков, которые используют множественное наследование, но обработка C++ его часто неуклюжа и иногда озадачивает.
Хотя я сталкивался со случаями, когда множественное наследование очень удобно, на самом деле они довольно редки. Это вероятно потому, что я предпочитаю использовать другие методы проектирования, когда мне действительно не нужно множественное наследование. Я предпочитаю избегать путаницы языковых конструкций, и легко создавать случаи наследования, когда вам нужно очень хорошо прочитать руководство, чтобы выяснить, что происходит.
Вам не следует "избегать" множественного наследования, но вы должны знать о проблемах, которые могут возникнуть, таких как "проблема с бриллиантами" ( http://en.wikipedia.org/wiki/Diamond_problem), и относиться к власти, предоставленной вам, с осторожностью., как вы должны со всеми силами.
Вы должны использовать это осторожно, есть некоторые случаи, например, проблема с бриллиантами, когда все может сложиться.
(источник: learncpp.com)
Мы используем Eiffel. У нас отличный МИ. Не волнуйтесь. Без вопросов. Легко управляемый. Есть времена, чтобы НЕ использовать МИ. Тем не менее, это полезно больше, чем люди понимают, потому что они: А) на опасном языке, который плохо справляется с этим - ИЛИ - Б) удовлетворены тем, как они работали в течение многих лет и лет - ИЛИ - В) другими причинами слишком много, чтобы перечислять, я вполне уверен - см. ответы выше).
Для нас, используя Eiffel, MI такой же естественный, как и все остальное, и еще один прекрасный инструмент в наборе инструментов. Честно говоря, мы совершенно не обеспокоены тем, что никто другой не использует Эйфелеву. Не волнуйтесь. Мы довольны тем, что имеем, и приглашаем вас посмотреть.
Пока вы смотрите: обратите особое внимание на безопасность пустот и устранение разыменования нулевого указателя. Пока мы все танцуем вокруг MI, ваши указатели теряются!:-)
Рискуя стать немного абстрактным, я нахожу интересным подумать о наследовании в рамках теории категорий.
Если мы вспомним все наши классы и стрелки между ними, обозначающие наследственные отношения, то что-то вроде этого
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
, Кроме того, и ближе к решению вопроса ОП, 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
а также C
, затем D
это класс, где R
может быть переписан как подкласс D
,
Убедитесь, что ваши кончики алмаза являются откатами и отжимами, дает нам хороший способ для общего решения проблем с именами или обслуживания, которые могут возникнуть в противном случае.
Обратите внимание, paercebal ответ paercebal вдохновил его, поскольку его намеки подразумевают вышеприведенную модель, учитывая, что мы работаем в полном классе категории всех возможных классов.
Я хотел обобщить его аргумент на то, что показывает, насколько сложные множественные наследственные отношения могут быть как мощными, так и не проблемными.
TL; DR. Думайте об отношениях наследования в вашей программе как о формировании категории. Тогда вы сможете избежать проблем с Diamond of Doom, выполнив множественные наследования и симметрично выполняя классы, создав общий родительский класс, который является откатом.
У каждого языка программирования есть немного различное отношение к объектно-ориентированному программированию с за и против. В версии 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" здесь не совпадает с параметризованным шаблоном класса (см. Эту ссылку для хорошего объяснения), но я думаю, что это справедливое использование терминологии.
Теперь я не хочу создавать впечатление, что это единственный способ безопасно использовать множественное наследование. Это всего лишь один способ, который довольно легко проверить.
Ключевая проблема с MI конкретных объектов заключается в том, что редко у вас есть объект, который на законных основаниях должен быть "быть A и быть B", поэтому это редко является правильным решением по логическим причинам. Гораздо чаще у вас есть объект C, который подчиняется "C может действовать как A или B", чего вы можете достичь с помощью наследования и компоновки интерфейса. Но не заблуждайтесь - наследование нескольких интерфейсов все еще является MI, только его подмножеством.
В частности, для C++ ключевым недостатком функции является не фактическое СУЩЕСТВОВАНИЕ множественного наследования, но некоторые конструкции, которые допускают это, почти всегда имеют дефекты. Например, наследование нескольких копий одного и того же объекта, например:
class B : public A, public A {};
НЕПРАВИЛЬНО ОПРЕДЕЛЕН. В переводе на английский это "B - это A и A". Так что даже в человеческом языке есть серьезная двусмысленность. Вы имели в виду "B имеет 2 As" или просто "B есть A"? Допуская такой патологический код и, что еще хуже, сделав его примером использования, С ++ не оказал никакой пользы, когда дело дошло до того, что он сохранил эту возможность на последующих языках.
Использование и злоупотребления наследования.
Статья делает большую работу по объяснению наследования, и это опасно.
Помимо ромбовидного рисунка, множественное наследование усложняет понимание объектной модели, что, в свою очередь, увеличивает затраты на обслуживание.
Композиция по своей сути проста для понимания, понимания и объяснения. Написание кода для него может быть утомительным, но хорошая IDE (прошло несколько лет с тех пор, как я работал с Visual Studio, но, безусловно, все IDE Java имеют отличные инструменты автоматизации сочетаний клавиш), должны преодолеть это препятствие.
Кроме того, с точки зрения обслуживания, "проблема алмазов" возникает и в случаях не буквального наследования. Например, если у вас есть A и B, и ваш класс C расширяет их оба, а у A есть метод 'makeJuice', который делает апельсиновый сок, а вы расширяете его, чтобы сделать апельсиновый сок с оттенком лайма: что происходит, когда дизайнер для ' B 'добавляет метод' makeJuice ', который генерирует электрический ток? "А" и "В" могут быть совместимыми "родителями" прямо сейчас, но это не значит, что они всегда будут такими!
В целом, принцип стремления избежать наследования, и особенно множественного наследования, является обоснованным. Как и во всех максимах, есть исключения, но вы должны убедиться, что мигающий зеленый неоновый знак указывает на любые кодируемые вами исключения (и тренируйте свой мозг так, чтобы каждый раз, когда вы видите такие деревья наследования, вы рисовали в своем собственном мигающем зеленом неоне знак), и что вы проверяете, чтобы убедиться, что все это имеет смысл время от времени.
Занимает 4/8 байт на каждый участвующий класс. (Один этот указатель на класс).
Это может никогда не вызывать беспокойства, но если однажды у вас будет структура микроданных, которая создается миллиарды раз, это будет.
Вы можете использовать композицию в предпочтении наследования.
Общее ощущение, что композиция лучше, и это очень хорошо обсуждается.