Является ли множественное наследование из того же базового класса через разные родительские классы действительно проблемой?

19

Я хочу реализовать иерархию наследования в своем движке на C ++, который имеет некоторые аналоги с Java:

  • все объекты наследуются от класса Object ,
  • некоторые функции предоставляются интерфейсами.

Это просто аналог , который дает мне некоторые преимущества ( не строковое сопоставление Java: C ++ 1: 1 , что невозможно).

Под «интерфейсом» я подразумеваю здесь класс с только чистыми виртуальными, общедоступными методами. Я знаю, что это не строгое и точное его определение.

Итак, я сделал:

class Object{...};

/* interfaces */
class Nameable : public Object{...};
class Moveable : public Object{...};
class Moveable3D : public Moveable{...};
class Moveable2D : public Moveable{...};

class Model3D : public Moveable3D, public Nameable{...}
class Line3D : public Moveable3D{...}
class Level: public Nameable{...}
class PathSolver : public Object{...}

Как вы можете видеть, Model3D теперь наследует от Object несколькими способами:

  • Moveable3D -> Moveable -> Object ,
  • Nameable -> Object .

И мой компилятор в VS2013 (VC ++) предупреждает меня об этом .

Это проблема?

Если да, то как его решить, чтобы получить лучшую структуру наследования?

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

  1. Удаление наследования между, например, Nameable и Object . Но таким образом Level потеряло бы свое наследование от Object тоже (и моя первая цель была: все объекты наследуются от Object class ).
  2. Я мог бы также удалить : public Object из каждого интерфейса . Но таким образом я был бы вынужден вручную набирать :public Object в каждом из многих конечных классов двигателей ( Model3D , Line3D , Level , PathSolver ). Таким образом, рефакторинг и создание новых конечных типов будут сложнее. Кроме того, информация о том, что каждый интерфейс будет гарантированно наследоваться от Object , теряется таким образом.

Я бы хотел избежать виртуального наследования (еще один уровень абстракции) , если , это не обязательно. Но может быть, это (по крайней мере, для : public Object )?

    
задан PolGraphic 18.12.2014 в 12:48
источник
  • @jxh Я не уверен, о чем вы просите. Но: любой Nameable гарантированно является Object (тот же для Moveable3D). Но Moveable3D не обязательно должен быть Nameable одновременно. Когда вы передаете Object *, вы не можете предположить, что это Nameable или Moveable3D, но вы можете, например. используйте dynamic_cast, чтобы проверить его. И, конечно, вы можете назначить любой другой тип объекту * (это было бы лучше). Более того, все интерфейсы имеют чистый виртуальный метод, поэтому вы не можете использовать их только с новым Nameable (), сначала вы должны наследовать их. Это то, о чем вы просили? –  PolGraphic 18.12.2014 в 12:56
  • Ваша текущая структура означает, что Model3D имеет два экземпляра объекта. Таким образом, если вы попытаетесь присвоить ему ссылку на объект, то вы получите ошибку неоднозначности, если вы не укажете, к какому объекту следует обращаться (к одному из именных или к движению Moveable3D). Но если вы используете виртуальное наследование, у вас будет только один объект (Nameable и Moveable3D, который будет иметь один и тот же экземпляр объекта). –  jxh 18.12.2014 в 12:59
  • Является ли объектом интерфейс? –  jxh 18.12.2014 в 13:13
  • @jxh это может быть, если это помогает - я только назначил такие методы, как виртуальная строка getDebugInfo () = 0; до сих пор, и я могу сделать гарантию сохранить его как интерфейс. –  PolGraphic 18.12.2014 в 13:15

5 ответов

20

Как сказал Юрай Блахо , канонический способ решения этой проблемы - виртуальное наследование. Одной из реализационных моделей виртуального наследования является добавление записи в vtable классов с использованием виртуального наследования, указывающего на реальный уникальный объект из базового класса. Таким образом, вы получите следующий граф наследования:

             Model3D
            /       \
     Moveable3D   Nameable
           |         |
      Moveable       |
            \       /
             Object

У вас должно быть:

class Nameable : public virtual Object{...};
class Moveable : public virtual Object{...};

Другим классам не требуется виртуальное наследование

Без виртуального наследования граф был бы

             Model3D
            /       \
     Moveable3D   Nameable
           |         |
      Moveable       |
           |         |
        Object    Object

с двумя различными экземплярами Object

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

Но реальная проблема заключается в том, что этот Dreadful Diamond on Derivation часто рассматривается как плохой дизайн иерархии и явно запрещен в Java.

Но IMHO, то, что C ++-виртуальное наследование делает под капотом, не так уж далека от того, что вы имели бы, если бы все ваши классы интерфейса были чистыми абстрактными классами (только чистые виртуальные общедоступные методы), не наследуемыми от Object, и если все ваши классы реализации наследовать явно из Object. Использование этого или нет зависит от вас: ведь это часть языка ...

    
ответ дан Serge Ballesta 18.12.2014 в 14:13
  • Великий. Пока мне нравится, что вы отвечаете больше всего :) Иллюстрация проблемы, решения и «осознайте ... [когда вы используете это решение]», часть делает его полным ответом. Спасибо, сэр. –  PolGraphic 18.12.2014 в 14:28
  • "явно запрещено в Java" Ну, Java не содержит множественного наследования вообще ... –  idmean 06.08.2017 в 12:37
4

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

class Object{...};

/* interfaces */
class Nameable : public virtual Object{...};
class Moveable : public virtual Object{...};
class Moveable3D : public virtual Moveable{...};
class Moveable2D : public virtual Moveable{...};

class Model3D : public virtual Moveable3D, public virtual Nameable{...}
class Line3D : public virtual Moveable3D{...}
class Level: public virtual Nameable{...}
class PathSolver : public virtual Object{...}

Без виртуального наследования в объекте есть несколько экземпляров одной и той же базы, что приведет к тому, что вы не сможете подставить конкретный тип, где нужен интерфейс:

void workWithObject(Object &o);

Model3D model;
workWithObject(model);
    
ответ дан Juraj Blaho 18.12.2014 в 13:31
  • Возможно, это решение здесь. Однако, я думаю, я могу уменьшить использование виртуального наследования до наследования от Object. Какие недостатки могут повлиять на использование виртуального наследования в обмен на решение проблемы? –  PolGraphic 18.12.2014 в 13:33
  • @PolGraphic: Да, вы можете ограничить это наследованием от Object. Современный подход для игрового движка C ++ не должен использовать наследование, а использовать композицию (компонент / сущность системы). Но это звучит слишком не по теме для заданного вопроса. Некоторая информация, например, по адресу: t-machine.org/index.php/2007/09/03/... –  Juraj Blaho 18.12.2014 в 14:07
  • спасибо :) И ссылка, которую вы предоставили, действительно стоит прочитать :) –  PolGraphic 18.12.2014 в 14:29
3

Да, это проблема: в макете памяти Model3D есть два подобъекта типа Object ; один является подобъектом Movable3D , другой из Nameable . Таким образом, эти два подобъекта могут содержать разные данные. Это особенно проблема, если класс объекта содержит функциональные возможности, связанные с идентификацией объекта. Например, класс Object , который реализует подсчет ссылок, должен быть уникальным подобъектом всех объектов; объект, который имеет два разных подсчета ссылок, очень вероятно, очень плохая идея ...

Чтобы избежать этого, используйте виртуальное наследование. Единственное виртуальное наследование, которое необходимо, - это базовый класс Object :

class Objec{...];

class Nameable : public virtual Object{...};
class Moveable : public virtual Object{...};
class Moveable3D : public Moveable{...};
class Moveable2D : public Moveable{...};

class Model3D : public Movable3D, public Nameable{...};
...
    
ответ дан cmaster 18.12.2014 в 13:43
  • Спасибо. Я вижу, как виртуальное наследование решает проблему здесь. Но мне также любопытно - есть ли проблемы, что виртуальное наследование вместо «нормального» приводит к моей конкретной модели в обмене? Было бы здорово, если бы вы могли это отрицать или коротко назвать любым из них (если это возможно). Тем не менее, отлично реагировать с приятным объяснением :) –  PolGraphic 18.12.2014 в 13:47
  • Единственным недостатком является несколько циклов ЦП дополнительной задержки при вызове указателей базового класса виртуальных функций / понижающих прогнозов: когда вы опускаете без виртуального наследования, указатель настраивается на постоянное количество байтов; с виртуальным наследованием, поиск из таблицы vtable необходим для нахождения правильного смещения. Это все. –  cmaster 18.12.2014 в 14:02
  • Спасибо. Вызов будет «длиннее» только для методов, которые наследуются практически от объекта или для любого виртуального метода, объявленного в любом классе (например, метод, объявленный в Moveable3D, когда только Moveable наследует фактически от Object)? –  PolGraphic 18.12.2014 в 14:32
  • Я не могу ответить на этот вопрос. Но я не думаю, что это актуально. Дополнительные накладные расходы - это загрузка одного значения из памяти, которое в любом случае находится в кеше, если вызов функции критически важен. Это определенно меньше, чем накладные расходы даже при вызове функции. –  cmaster 18.12.2014 в 17:31
2

Виртуальное наследование будет наводить порядок в вашей иерархии классов. Несколько вещей, на которые нужно следить:

  • Не смешивайте виртуальное / не виртуальное наследование для того же базового класса в своей иерархии, если вы действительно не знаете, что делаете.
  • Порядок инициализации становится сложным. Конструктор фактически унаследованного класса будет вызываться только один раз. Конструктор будет принимать аргументы из самого производного класса. Если вы не укажете его явно в Moveable3D, будет вызываться конструктор по умолчанию, даже если Moveable заданы аргументы для конструктора Object. Таким образом, чтобы избежать беспорядка, убедитесь, что вы используете только конструкторы DEFAULT во всех классах, которые вы наследуете практически.
ответ дан Romanoff 18.12.2014 в 13:57
1

Сначала убедитесь, что Model3D (который использует множественное наследование) должен обеспечить интерфейс как от его базовых классов, так и от co_de%, а также от Nameable . Потому что из имени, которое вы предоставили своему классу, мне кажется, что Model3D лучше реализовать в качестве члена Nameable .

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

    
ответ дан ravi 18.12.2014 в 13:03
  • За исключением того, что у них нет указателей или ссылок на объект. –  jrok 18.12.2014 в 13:04
  • @ravi благодарит вас за предложение проверить возможность сдерживания vs наследования в моей модели. Это сложнее, чем то, что я поставил под вопрос (чтобы сделать его читаемым), но это прекрасный момент во многих местах моей структуры. Однако, я не думаю, что это лекарство во всех них. До сих пор проголосовали - я не буду принимать его еще, чтобы дать шанс представить другие идеи :) –  PolGraphic 18.12.2014 в 13:09