The C ++ language for all user-defined classes and structures generates, by default, a copy constructor and a copy assignment operator. Thus, for an important number of cases, the programmer is relieved of writing these functions manually. For example, default operators work well for structures that contain data. At the same time, data can be stored both in simple types and in complex containers, such as std :: vector or std :: string.
In light of this, it would be convenient to have the structure comparison operators == and! = By default, but the C ++ compiler, in accordance with the standard, does not generate them.
It is easy to write the operator of term-by-term comparison of structures, but such an organization of the program is inconvenient and dangerous from the point of view of errors. For example, if a programmer adds a new member to the structure, but forgets to add an appropriate comparison in the user comparison operator, then a rather difficult to diagnose error will appear in the program. Moreover, the structure declaration and the user comparison operator are usually separated from each other, since they are located in different files (* .h and * .cpp).
Automating the writing of term-based comparison operators in C ++ is not easy, as there are no means in this language that allow the program to determine how many and which members are contained in the structure.
')
In the mid-2000s, working on a large project that was constantly evolving and demanding frequent changes in data structures, I set out to solve the question of comparison operators once and for all. As a result, a C ++ construct with the use of macros was created, which allows declaring structures followed by the automatic generation of their term-based operators. The same construction allowed to automatically implement other advanced operations: loading and saving data to files. I offer it to your attention.
Other existing solutions
At the moment I know the following alternative solutions to the problem described:
- The use of dynamic structures. Instead of the usual C ++ structure, a container of heterogeneous elements is used that are cast to a single type. For example, type VARIANT from Windows OLE. A string container is also used to store member names. Thus, the names of members, their types and number are made available to the program at run time. However, this approach leads to costs during program execution for access to members of such a structure. The access syntax of the object.member_name or pObject-> member_name type becomes unavailable, it has to be changed to something like object.at (“member_name”). In addition, there is a linear increase in memory consumption: each instance of a structure takes up more memory space than a regular (static) structure.
- Using the boost library, namely, the container boost :: fusion :: map. Here it was possible to charge all costs to the compiler, however, the traditional syntax of access to the members could not be saved. It is necessary to use constructions of the form: at_key <member_name> (object).
- C ++ code generation. The description of the structure in C ++ and its comparison operator is not written by the programmer manually, but is generated by the script based on the description of the structure in some other input language. This approach, from my point of view, is ideal, but at the moment I have not implemented it, so the article is not about it.
Macro based solution
The solution that I managed to implement using macros has the following advantages:
- There is no load at run time to access members of the structure.
- It was possible to preserve the standard syntax for accessing members of the structure of the object.member_name or pObject-> member_name type.
- Load on memory of type O (1). In other words, each instance of an auto-compare structure takes up as much memory space as a regular structure. There are only fixed (small) memory costs for each type of such structures being declared.
Among the shortcomings are the following:
- The presence in the structure of additional service members, which reduces the convenience of analysis tools such as Intellisense or DoxyGen.
- The possibility of conflicts of names of service members with user.
- The impossibility of initializing the list of initializers of the form struct a = {1,2,3}.
Usage example
Suppose we need to create a structure for storing data about people, equivalent to the following ordinary structure:
struct MANPARAMS { std::string name; int age; std::vector<std::string> friend_names; double karma; };
On the basis of my library, the structure with automatic rented operations is declared as follows:
class AUTO_MANPARAMS { PARAMSTRUCT_DECLARE_BEGIN(AUTO_MANPARAMS); public: DECLARE_MEMBER_PARAMSTRUCT(std::string, name); DECLARE_MEMBER_PARAMSTRUCT(int, age); DECLARE_MEMBER_PARAMSTRUCT(std::vector<std::string>, friend_names); DECLARE_MEMBER_PARAMSTRUCT(double, karma); };
After that, once for the whole program, you need to compile the following macro call in one of the * .cpp files:
PARAMFIELD_IMPL(AUTO_MANPARAMS);
Everything! Now you can safely use these structures as usual, and compare them for equality or inequality, without worrying about writing the corresponding operators. For example:
void men(void) { AUTO_MANPARAMS man1, man2; man1.name = “John Smith”; man1.age = 18; man1.karma = 0; man2.name = “John Doe”; man2.age = 36; man2.karma = 1; man2.friends.push_back(“Sergud Smith”); if(man1 == man2) printf(“Ku-ku!\n”); }
Implementation
As can be seen from the above, at the beginning of the definition of each structure, you need to call the PARAMSTRUCT_DECLARE_BEGIN (x) macro, which will define some common types and static service members for this structure. After this, when declaring each user member, call the second macro, DECLARE_MEMBER_PARAMSTRUCT (type, name), which, in addition to declaring the member with the specified name, defines the service members of the structure associated with it.
Basic ideas for implementation:
- For each member of the structure, a function is automatically generated comparing this member.
- Pointers to comparison functions are stored in a static array. The comparison operator simply iterates through all the elements of this array and calls the comparison function for each member.
- When the program starts, this array is initialized so as not to duplicate the code declared by the members of the structure.
1. Autogeneration of comparison functions of each member
Each such function is a member of the structure and makes a comparison of "its" data member. It is generated in the macro DECLARE_MEMBER_PARAMSTRUCT (type, name) as follows:
bool comp##name(const ThisParamFieldClass& a) const \ { \ return name == a.name; \ } \
Where ThisParamFieldClass is the type of our structure, which is declared via typedef in the head macro - see below.
2. Array with pointers to comparison functions
The head macro PARAMSTRUCT_DECLARE_BEGIN (x) declares a static array in which pointers to each of the member comparison functions are stored. For this, their type is first determined:
#define PARAMSTRUCT_DECLARE_BEGIN(x) \ private: \ typedef x ThisParamFieldClass; \ typedef bool (ThisParamFieldClass::*ComFun)(const ThisParamFieldClass& a) const; \ struct MEM_STAT_DATA \ { \ std::string member_name; \ ComFun comfun; \ }; \
And then the array is declared:
static std::vector<MEM_STAT_DATA> stat_data; \
Here are the comparison operators:
public: \ bool operator==(const ThisParamFieldClass& a) const; \ bool operator!=(const ThisParamFieldClass& a) const { return !operator==(a); } \
The comparison operator is implemented by another macro (PARAMFIELD_IMPL), but its implementation is trivial if there is a filled array of stat_data: you just need to call the comparison function for each element of this array.
For comparison of structures alone, there is no need to store the names of members of the structure in an array. However, storing names allows you to extend the concept, applying it not only to term comparisons, but also to other operations, such as saving and loading in text format suitable for human reading.
3. Filling in data about members of the structure
It remains to resolve the issue of filling in the array stat_data. Since the information about members is initially unavailable anywhere, except for the macro DECLARE_MEMBER_PARAMSTRUCT, it is possible to fill the array only from there (directly or indirectly). However, this macro is called inside the declaration of the structure, which is not the most convenient place to initialize std :: vector. I solved this problem with service objects. For each member of the structure, a utility class and an object of this class are declared. This class has a constructor - it adds the information about the element to the static array stat_data:
class cl##name \ { \ public: \ cl##name(void) \ { \ if(populate_statdata) \ { \ MEM_STAT_DATA msd = \ { \ #name, \ &ThisParamFieldClass::comp##name \ }; \ stat_data.push_back(msd); \ } \ } \ }; \ cl##name ob##name;
where populate_statdata is a static flag that is declared in the head macro and indicates whether the array stat_data should be filled with the names of the members of the structure and functions of their comparison. When the program starts, the initialization mechanism described below sets populate_statdata = true and creates one instance of the structure. In this case, the constructors of the service objects associated with each member of the structure fill the array with data about the members. After that, populate_statdata = false is set, and the static member information array is no longer changed. This solution leads to some loss of time each time the structure is created by a user program, to check the populate_statdata flag. However, the memory consumption does not increase: the service object does not contain data members, only the constructor.
Finally, the populate_statdata flag control mechanism: implemented using a static service object with a constructor, one for the entire structure. This object is declared in the head macro:
class VcfInitializer \ { \ public: \ VcfInitializer(void); \ }; \ static VcfInitializer vcinit;
The constructor implementation is in the PARAMFIELD_IMPL (x) macro:
x::VcfInitializer::VcfInitializer(void) \ { \ x::populate_statdata = true; \ ThisParamFieldClass dummy; \ x::populate_statdata = false; \ } \
Full Macro Text
#define PARAMSTRUCT_DECLARE_BEGIN(x) \ private: \ typedef x ThisParamFieldClass; \ typedef bool (ThisParamFieldClass::*ComFun)(const ThisParamFieldClass& a) const; \ struct MEM_STAT_DATA \ { \ std::string member_name; \ ComFun comfun; \ }; \ static std::vector<MEM_STAT_DATA> stat_data; \ static bool populate_statdata; \ public: \ bool operator==(const ThisParamFieldClass& a) const; \ bool operator!=(const ThisParamFieldClass& a) const { return !operator==(a); } \ private: \ class VcfInitializer \ { \ public: \ VcfInitializer(void); \ }; \ static VcfInitializer vcinit; #define DECLARE_MEMBER_PARAMSTRUCT(type,name) \ public: \ type name; \ private: \ bool comp##name(const ThisParamFieldClass& a) const \ { \ return name == a.name; \ } \ class cl##name \ { \ public: \ cl##name(void) \ { \ if(populate_statdata) \ { \ MEM_STAT_DATA msd = \ { \ #name, \ &ThisParamFieldClass::comp##name, \ }; \ stat_data.push_back(msd); \ } \ } \ }; \ cl##name ob##name; #define PARAMFIELD_IMPL(x) \ std::vector<x::MEM_STAT_DATA> x::stat_data; \ bool x::populate_statdata = false; \ x::VcfInitializer x::vcinit; \ x::VcfInitializer::VcfInitializer(void) \ { \ x::populate_statdata = true; \ ThisParamFieldClass dummy; \ x::populate_statdata = false; \ } \ bool x::operator==(const x& a) const \ { \ bool r = true; \ for(size_t i=0; r && i<stat_data.size(); i++) \ { \ r = (this->*stat_data[i].comfun)(a); \ } \ return r; \ }
Conclusion
On the basis of the above macros, you can declare structures for which comparison operators and other perennial operations are automatically created. Other such operations include, for example, loading and saving to text files in XML format. Lack of duplication of code facilitates the work and protects against errors. The declaration of a member of the structure alone adds this member to the compare, save, and load operations.