Все основано на реальных событиях, но примеры были максимально упрощены, чтобы в них осталась лишь суть проблемы.
Итак, в разрабатываемом приложении использовалось большое количество единообразно обрабатываемых сущностей. Причем, одни из них были отображаемыми, другие требовалось постоянно обновлять, третьи соединяли в себе и то и другое. Соответственно появилось желание реализовать три базовых класса
- Renderable: содержит признак видимости и метод рисования
- Updatable: содержит признак активности и метод обновления состояния
- VisualActivity = Renderable + Updatable
Добавлю еще два искусственных класса, чтобы продемонстрировать случившиеся сложности
- JustVisible: просто видимый объект
- JustVisiblePlusVisualActivity: JustVisible с обновляемым состоянием
Получается следующая картина
Сразу же видна проблема — конечный класс наследует Renderable дважды: как родитель JustVisible и VisualActivity. Это не дает нормально работать со списками отображаемых объектов
JustVisiblePlusUpdate* object = new JustVisiblePlusUpdate; std::vector<Renderable*> vector_visible; vector_visible.push_back(object);
Получается неоднозначность (ambiguous conversions) — компилятор не может понять, об унаследованном по какой ветке Renderable идет речь. Ему можно помочь, уточнив направление путем явного приведения типа к одному из промежуточных
JustVisiblePlusUpdate* object = new JustVisiblePlusUpdate; std::vector<Renderable*> vector_visible; vector_visible.push_back(static_cast<VisualActivity*>(object));
Компиляция пройдет успешно, только вот ошибка останется. В нашем случае требовался один и тот же Renderable вне зависимости от того, каким образом он был унаследован. Дело в том, что в случае обычного наследования в классе-потомке (JustVisiblePlusVisualActivity) содержится отдельный экземпляр родительского класса для каждой ветки.
Причем свойства каждого из них можно менять независимо. Выражаясь на c++, истинно выражение
(&static_cast<VisualActivity*>(object)->mVisible) != (&static_cast<JustVisible*>(object)->mVisible)
Так что обычное множественное наследование для задачи не подходило. А вот виртуальное выглядело той самой серебряной пулей, которая была нужна… Все что требовалось — унаследовать базовые классы Renderable и Updatable виртуально, а остальные — обычным образом:
class VisualActivity : public virtual Updatable, public virtual Renderable ... class JustVisible : public virtual Renderable ... class JustVisiblePlusUpdate : public JustVisible, public VisualActivity
Все унаследованные виртуально классы представлены в потомке только один раз. И все бы работало, если бы базовые классы не имели конструкторов с параметрами. Но такие конструкторы существовали, и случился сюрприз. Каждый виртуально наследуемый класс имел как конструктор по умолчанию так и параметризованный
class Updatable { public: Updatable() : mActive(true) { } Updatable(bool active) : mActive(active) { } //.... };
class Renderable { public: Renderable() : mVisible(true) { } Renderable(bool visible) : mVisible(visible) { } //.... };
Классы-потомки содержали только конструкторы с параметрами
class VisualActivity : public virtual Updatable, public virtual Renderable { public: VisualActivity(bool visible, bool active) : Renderable(visible) , Updatable(active) { } //.... };
class JustVisible : public virtual Renderable { public: JustVisible(bool visible) : Renderable(visible) { } //.... };
class JustVisiblePlusUpdate : public JustVisible, public VisualActivity { public: JustVisiblePlusUpdate(bool visible, bool active) : JustVisible(visible) , VisualActivity(visible, active) { } //.... };
И все равно при создании объекта
JustVisiblePlusUpdate* object = new JustVisiblePlusUpdate(false, false);
вызывался конструктор Renderable по умолчанию! На первый взгляд, это казалось чем-то диким. Но рассмотрим подробнее, откуда взялось предположение, что приведенный код должен приводить к вызову конструктора Renderable::Renderable(bool visible) вместо Renderable::Renderable().
Породило проблему допущение, что Renderable чудесным образом разделится между JustVisible, VisualActivity и JustVisiblePlusUpdate. Но «чуду» не суждено было случиться. Ведь тогда можно было бы написать что-то типа
class JustVisiblePlusUpdate : public JustVisible, public VisualActivity { public: JustVisiblePlusUpdate(bool active) : JustVisible(true) , VisualActivity(false, active) { } //.... };
сообщив компилятору противоречивую информацию, когда одновременно требовалось бы конструирование Renderable с параметрами true и false. Открывать возможность для подобных парадоксов никто не захотел, соответственно и механизм работает другим образом. Класс Renderable в нашем случае больше не является частью ни JustVisible, ни VisualActivity, а принадлежит непосредственно JustVisiblePlusUpdate.
Это объясняет, почему вызывался конструктор по умолчанию — конструкторы виртуальных классов должны вызываться конечными наследниками, т.е. рабочим вариантом было бы что-то типа
class JustVisiblePlusUpdate : public JustVisible, public VisualActivity { public: JustVisiblePlusUpdate(bool visible, bool active) : JustVisible(visible) , VisualActivity(visible, active) , Renderable(visible) , Updatable(active) { } //.... };
При виртуальном наследовании приходится, кроме конструкторов непосредственных родителей, явно вызывать конструкторы всех виртуально унаследованных классов. Это не очень очевидно и с легкостью может быть упущено в нетривиальном проекте. Так что лишний раз подтвердилась истина: не больше одного открытого наследования для каждого класса. Оно того не стоит. В нашем случае было принято решение отказаться от разделения на Renderable и Updatable, ограничившись одним базовым VisualActivity. Это добавило некоторую избыточность, но резко упростило общую архитектуру — отслеживать и поддерживать все виртуальные и обычные случаи наследования было слишком затратно.
ссылка на оригинал статьи http://habrahabr.ru/post/185826/
Добавить комментарий