Подтвердить что ты не робот

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

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

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

Это просто аналогия, которая дает мне некоторые преимущества ( не строковое сопоставление Java: С++ 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 (VС++) предупреждает меня об этом.

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

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

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

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

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

4b9b3361

Ответ 1

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

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

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

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

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


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

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

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

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

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

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

Ответ 2

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

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);

Ответ 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{...};
...

Ответ 4

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

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

Ответ 5

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

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