Одна из неприятных проблем — при внесении даже простых изменений приходится править код в нескольких местах. Например, если в класс добавляется поле данных, его нужно добавить в объявление класса, инициализировать в конструкторе(ах), а если переопределены операторы копирования или сравнения — то и в них тоже. Это отнимает время и приводит к ошибкам, если забыть про одно из мест (особенно неприятно забыть инициализировать, такие ошибки могут жить годами, и вызывать неожиданные, трудновоспроизводимые проблемы).
Обычно, если класс предполагает активную модификацию полей, то пишется макрос, который берёт реализацию действий с полем (инициализацию, копирование, сериализацию, reflection) на себя.
В результате переменная должна прописываться всего в двух местах — объявляться в классе и реализовываться (или регистрироваться для последующего использования в реализации).
Получается что-то вроде:
class TData
{
public:
int Number;
float Factor;
BEGIN_FIELDS
FIELD(Number, 0)
FIELD(Factor, 1.0f)
END_FIELDS
};
Реализацию макросов не привожу, это может быть, например, регистрация указателей на поля данных, их имён и начальных значений для последующего использования.
Ещё один пример, попроще. Нужно сделать отражение перечисления, например, сопоставить варианту перечисления строку его имени. Обычно это делается как-то так:
enum TModelType
{
Car,
Weapon,
Human
};
#define REFLECT_MODEL_TYPE(mac_value) Register(mac_value, #mac_value);
void TModelTypeReflection::RegisterTypes()
{
REFLECT_MODEL_TYPE(Car)
REFLECT_MODEL_TYPE(Weapon)
REFLECT_MODEL_TYPE(Human)
}
Объявление TModelTypeReflection и реализацию Register предоставлю воображению читателя.
Довольно долго я довольствовался таким положением дел. Но недавно подумал, что можно сделать и получше, обойдясь единственным объявлением. Сделать это можно с помощью всё тех же макросов.
Для последнего примера это будет выглядеть так:
#define DECLARE_MODEL_TYPE(mac_value, mac_next)
mac_value,
mac_next
Register(mac_value, #mac_value);
#define END_MODEL_TYPE
}; void TModelTypeReflection::RegisterTypes() {
enum TModelType
{
DECLARE_MODEL_TYPE(Car,
DECLARE_MODEL_TYPE(Weapon,
DECLARE_MODEL_TYPE(Human,
END_MODEL_TYPE)))
}
Макросы DECLARE_MODEL_TYPE развернутся сначала в элементы перечисления, затем код из END_MODEL_TYPE закроет блок перечисления и вставит заголовок функции, дальше в тело функции вставятся вызовы Register для элементов, только в обратном порядке, и наконец фигурная скобка закроет блок функции (поэтому она и без точки с запятой).
Похожий код можно написать и для полей класса.
Осталось только сказать о недостатках:
- регистрация в обратном порядке, но если порядок всё же важен, можно это учесть в реализации Register, например, добавлять очередное поле в начало списка, а не в конец;
- проблемы с системами автоматической генерацией документации вроде DOxygen, они не догадаются развернуть макросы;
- необходимость при добавлении поля добавлять ещё одну закрывающую скобку после END_MODEL_TYPE. Неприятный недостаток, т.к. править код приходится всё равно в двух местах. Немного радует, что забыть о скобке не позволит препроцессор.
Альтернативное решение, позволяющее объединить объявление с реализацией — использование кодогенератора, но и этот подход тоже имеет свои недостатки.
Автор: Polsky