Статья о том, как множественное наследование все усложняет. Как виртуальное наследование, на первый взгляд, реализовано нелогично. Как на второй взгляд логика появляется, но уровень сложности и запутанности продолжает расти. В общем, чем сложнее задача, тем более простые нужно подбирать инструменты.
Все основано на реальных событиях, но примеры были максимально упрощены, чтобы в них осталась лишь суть проблемы.
Итак, в разрабатываемом приложении использовалось большое количество единообразно обрабатываемых сущностей. Причем, одни из них были отображаемыми, другие требовалось постоянно обновлять, третьи соединяли в себе и то и другое. Соответственно появилось желание реализовать три базовых класса
- 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. Это добавило некоторую избыточность, но резко упростило общую архитектуру — отслеживать и поддерживать все виртуальные и обычные случаи наследования было слишком затратно.
Автор: vadim_ig