Грабли 2: Виртуальное наследование

в 4:37, , рубрики: c++, inheritance, KISS, наследование, Программирование, Проектирование и рефакторинг, метки: , ,

Статья о том, как множественное наследование все усложняет. Как виртуальное наследование, на первый взгляд, реализовано нелогично. Как на второй взгляд логика появляется, но уровень сложности и запутанности продолжает расти. В общем, чем сложнее задача, тем более простые нужно подбирать инструменты.

Все основано на реальных событиях, но примеры были максимально упрощены, чтобы в них осталась лишь суть проблемы.

Итак, в разрабатываемом приложении использовалось большое количество единообразно обрабатываемых сущностей. Причем, одни из них были отображаемыми, другие требовалось постоянно обновлять, третьи соединяли в себе и то и другое. Соответственно появилось желание реализовать три базовых класса

  1. Renderable: содержит признак видимости и метод рисования
  2. Updatable: содержит признак активности и метод обновления состояния
  3. VisualActivity = Renderable + Updatable

Добавлю еще два искусственных класса, чтобы продемонстрировать случившиеся сложности

  1. JustVisible: просто видимый объект
  2. JustVisiblePlusVisualActivity: JustVisible с обновляемым состоянием

Получается следующая картина
Грабли 2: Виртуальное наследование

Сразу же видна проблема — конечный класс наследует 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) содержится отдельный экземпляр родительского класса для каждой ветки.
Грабли 2: Виртуальное наследование

Причем свойства каждого из них можно менять независимо. Выражаясь на 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().
Грабли 2: Виртуальное наследование

Породило проблему допущение, что 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.
Грабли 2: Виртуальное наследование

Это объясняет, почему вызывался конструктор по умолчанию — конструкторы виртуальных классов должны вызываться конечными наследниками, т.е. рабочим вариантом было бы что-то типа

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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js