Пару лет назад я столкнулся с ситуацией, в которой получалась не очень красивая структура кода. Пришлось подумать и улучшением её, и сейчас я хочу поделиться своим решением. Если эта проблема уже имеет известное решение, дайте ссылку. Будет интересно сравнить со своим решением.
Итак, задача: имеем класс с несколькими конструкторами, с различными комбинациями параметров (т.е. несколько возможностей инизиализации объектов), и при наследовании от этого класса нужно, чтобы конструкторы наследника поддерживали все эти параметры, т.е. все варианты инициализации предка:
Code:
class B
{
public:
B() {...}
B(int i, double d, const std::string& s) {...}
B(double d) {...}
B(char* p, int n) {...}
};
class D:
public B
{
public:
D() {...}
D(/* the base initialization parameters: */ int i, double d, const std::string& s, /* parameters for class D: */ int x, int y):
B(i, d, s)
{...}
D(double d, int x, int y):
B(d)
{...}
D(char *p, int n, int x, int y):
B(p, n)
{...}
};
Как видим, приходится повторять конструктор потомка столько раз, сколько конструкторов у предка. И при каждом добавлении конструкторов в B потребуется вносить соответствующие изменения в D. Не говоря о том, что и у D может быть несколько способов инициализации, и от D тоже могут наследоваться.
Для решения этой проблемы я выделил инициализационные параметры в отдельную структуру и все конструкторы заменил одним, которому передаётся эта структура (более того, для удобства инициализации этой структуры конструкторы B перемещаются в неё):
Code:
class B
{
public:
struct InitData
{
int i_;
double d_;
std::string s_;
InitData(int i, double d, const std::string& s): i_(i), d_(d), s_(s) { }
InitData(double d): d_(d) { }
InitData(char* str, int n) s_(str, n) { }
};
B() {...}
B(const InitData& data) {...}
};
class D:
public B
{
public:
D() {...}
D(const B::InitData& baseData, int x, int y):
B(baseData)
{...}
};
Дабы класс D не стал источником тех же проблем для своих потомков, с его инициализационные параметрами поступаем так же:
Code:
class D: public class B
{
public:
struct InitData
{
int x;
int y;
};
D() {...}
D(const B::InitData& baseData, const InitData& data): B(baseData) {...}
};
Немного лучше, но не до конца.
1. Число параметров конструктора класса D зависит от количества предков (как прямых, в результате множественного наследования, так и косвенных, пришедших через цепочку наследований). Изменения в иерархии будут приводить к изменениям в сигнатуре конструктора, поэтому желательно перейти к конструктору с одним параметром.
2. Полученная схема не даёт возможности инициализировать по умолчанию часть базовых классов или часть членов (как базовых классов, так и нижнего класса).
3. Если каждая структура InitData повторяет члены соответствующего класса (в большинстве случаев так и будет), то можно ли избежать этой дубликации?
1. Первая проблема решается объединением всех структур InitData в одну:
Code:
class D: public class B1, public class B2
{
public:
struct InitData: B1::InitData, B2::InitData
{...};
D() {...}
D(const InitData& data): B1(data), B2(data) {...}
};
Здесь нужно обратить внимание на одну вещь. Т.к. конструкторы классов B1, B2 и D были переведены в соотвествующие структуры InitData, то иерархия InitData будет обладать той же проблемой, которую мы изначально хотим решить. Поэтому от конструкторов в этих структурах придётся отказаться.
2. Проблему инициализации по умолчанию некторых предков или членов можно решить двумя путями. В тех случаях, когда каждому члену InitData соответствует член в соответствующем классе, можно просто инициализировать эти члены в InitData по умолчанию. В частности, если вся структура инициализирована по умолчанию, то это будет соответствовать вызову конструктора по умолчанию для соответствующего класса, и тогда конструктор B::B(const InitData& d = InitData()) полностью вытеснит конструктор по умолчанию. В остальных же случаях в структуры InitData придётся добавить специальное поле, указывающее, какие члены инициализированы нормальными данными.
3. Дублирования членов можно избежать, если каждый класс будет унаследован от своих инициализационных данных. При этом потребуется виртуальное наследование:
Code:
struct BData
{
//...
};
class B:
public virtual BData
{
//...
};
struct DData:
virtual BData
{
//...
};
class D:
public B, public virtual DData //виртуальное наследование на случай, если у D будут свои наследники
{
//...
};