One of the unpleasant problems is that when making even simple changes, you have to edit the code in several places. For example, if a data field is added to a class, it must be added to the class declaration, initialized in the constructor (s), and if the copy or comparison operators are redefined, so are they. This takes time and leads to errors, if you forget about one of the places (it is especially unpleasant to forget to initialize, such errors can live for years, and cause unexpected, hard-to-reproduce problems).
Usually, if a class assumes active modification of fields, then a macro is written that takes the implementation of field actions (initialization, copying, serialization, reflection) onto itself.
As a result, the variable must be registered in only two places - declared in the class and implemented (or registered for later use in the implementation).
It turns out something like:
class TData { public: int Number; float Factor; BEGIN_FIELDS FIELD(Number, 0) FIELD(Factor, 1.0f) END_FIELDS };
I do not provide implementation of macros, for example, it can be registration of pointers to data fields, their names and initial values for later use.
Another example, simpler. You need to make a reflection of the enumeration, for example, to associate with the enumeration variant a string of its name. This is usually done something like this:
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) }
The
TModelTypeReflection declaration and the
Register implementation will provide the reader with imagination.
')
For a long time I was content with this state of affairs. But recently I thought that you can do better, bypassing the only announcement. This can be done using all the same macros.
For the last example, it would look like this:
#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))) }
The
DECLARE_MODEL_TYPE macros
are expanded first into the enumeration elements, then the code from
END_MODEL_TYPE closes the enumeration block and inserts the function header, then the
Register calls for the elements are inserted in the function body, only in reverse order, and finally the curly bracket closes the function block (therefore, it also has ).
Similar code can be written for class fields.
It remains only to say about the shortcomings:
- registration in the reverse order, but if the order is still important, you can take this into account in the Register implementation, for example, add the next field to the top of the list, and not to the end;
- problems with automatic generation of documentation like DOxygen, they won’t guess how to deploy macros;
- the need to add one more closing bracket when END_MODEL_TYPE when adding a field. Unpleasant disadvantage, because I still have to edit the code in two places. A little happy that the preprocessor will not allow to forget about the parenthesis.
An alternative solution that allows combining an advertisement with an implementation is the use of a code generator, but this approach also has its drawbacks.