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


Является ли хорошей концепцией использование множественного наследования или я могу делать другие вещи вместо этого?



Answers


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

Резюме

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

В-третьих, объектная ориентация велика, но это не The Only Truth Out There 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 полностью, полностью, полностью НЕРАСПРОСТРАНЕННО ... Это резко снижает опасность множественного наследования)

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

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

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

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

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, и иногда класс будет типом нескольких разных классов, и иногда важно это отразить.

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

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

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




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







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

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

Как это часто бывает в C ++, если вы следуете основному руководству, вы можете безопасно использовать его, не «постоянно переглядывая свое плечо». Основная идея заключается в том, что вы различаете особый вид определения класса, называемый «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» здесь не совпадает с параметризованным классом шаблона (см. Эту ссылку для хорошего объяснения), но я считаю, что это справедливое использование терминологии.

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




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

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

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

Теперь проблемы, связанные с Diamond of Death, здесь, когда C и B разделяют некоторые имена свойств / методов, и все становится неоднозначным; однако, если мы переместим какое-либо общее поведение в A тогда двусмысленность исчезнет.

Полагая категорические термины, мы хотим, чтобы A , B и C были такими, что если B и C наследуются от Q тогда A можно переписать как подкласс Q Это делает A что-то называемым pushout .

Существует также симметричная конструкция на D называемая откатом . Это по существу самый общий полезный класс, который вы можете построить, который наследуется как от B и от C То есть, если у вас есть какой-либо другой класс R наследуемый от B и C , то D - класс, где R можно переписать как подкласс D

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

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

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

TL; DR Представьте, что отношения наследования в вашей программе образуют категорию. Затем вы можете избежать проблем с Diamond of Doom, сделав многонаследованные классы pushouts и симметрично, создав общий родительский класс, который является откатом.







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

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

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

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




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

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

class B : public A, public A {};

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




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

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




он занимает 4/8 байт на каждый класс. (Один указатель на класс).

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




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

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

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