Магия макросов для объединения объявления и реализации

в 6:35, , рубрики: c++, Программирование, метки:

Одна из неприятных проблем — при внесении даже простых изменений приходится править код в нескольких местах. Например, если в класс добавляется поле данных, его нужно добавить в объявление класса, инициализировать в конструкторе(ах), а если переопределены операторы копирования или сравнения — то и в них тоже. Это отнимает время и приводит к ошибкам, если забыть про одно из мест (особенно неприятно забыть инициализировать, такие ошибки могут жить годами, и вызывать неожиданные, трудновоспроизводимые проблемы).

Обычно, если класс предполагает активную модификацию полей, то пишется макрос, который берёт реализацию действий с полем (инициализацию, копирование, сериализацию, 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

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


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