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

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

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

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

То есть вы должны иметь:

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

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

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

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

с двумя различными экземплярами объекта

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

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

Но ИМХО, то, что виртуальное наследование C ++ делает под капотом, не так уж далеко от того, что вы имели бы, если бы все ваши интерфейсные классы были чистыми абстрактными классами (только чистые виртуальные публичные методы), не наследовавшими от 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 (который использует множественное наследование) должен предоставлять интерфейс как от своих базовых классов, т. е. Nameable , так и от Model3D . Поскольку из имени, которое вы указали своему классу, мне кажется, что Nameable лучше реализовать как член 'Model3D' .

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

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