В свете этого удобно было бы иметь и операторы сравнения структур == и != по умолчанию, однако компилятор C++, в соответствии со стандартом, не генерирует их.
Написать оператор почленного сравнения структур несложно, однако такая организация программы является неудобной и опасной с точки зрения ошибок. Скажем, если программист добавит новый член в структуру, но забудет добавить соответствующее сравнение в пользовательском операторе сравнения — то в программе образуется довольно трудно диагностируемая ошибка. Тем более что объявление структуры и пользовательский оператор ее сравнения обычно разнесены друг от друга, так как находятся в разных файлах (*.h и *.cpp).
Автоматизировать написание операторов почленного сравнения в рамках C++ непросто, так как в этом языке отсутствуют средства, позволяющие во время работы программы выяснить, сколько и каких членов содержится в структуре.
В середине 2000х годов, работая над крупным проектом, который постоянно эволюционировал и требовал частых изменений структур данных, я задался целью решить вопрос операторов сравнения раз и навсегда. В результате была создана конструкция на C++ с применением макросов, позволяющая объявлять структуры с последующей автоматической генерацией операторов почленного сравнения их. Эта же конструкция позволила автоматически реализовать и другие почленные операции: загрузка и сохранение данных в файлы. Предлагаю ее вашему вниманию.
Другие существующие решения
На данный момент мне известны следующие альтернативные решения описанной проблемы:
- Использование динамических структур. Вместо обычной структуры C++ применяется контейнер разнородных элементов, которые приведены к единому типу. Например, типу VARIANT из Windows OLE. Также используется контейнер строк для хранения имен членов. Тем самым имена членов, их типы и количество делаются доступными программе во время выполнения. Однако такой подход приводит к затратам во время выполнения программы на доступ к членам такой структуры. Синтаксис доступа вида object.member_name или pObject->member_name становится недоступным, его приходится менять на что-нибудь вроде object.at(“member_name”). Кроме того, имеется линейный рост потребления памяти: каждый экземпяр структуры занимает больше места в памяти, чем обычная (статическая) структура.
- Использование библиотеки boost, а именно, контейнера boost::fusion::map. Здесь удалось все затраты взвалить на плечи компилятора, однако традиционный синтаксис доступа к членам сохранить не удалось. Приходится пользоваться конструкциями вида: at_key<member_name>(object).
- Генерация кода C++. Описание структуры на C++ и оператора ее сравнения не пишется программистом вручную, а генерируется скриптом на основе описания структуры на каком-то другом входном языке. Данный подход, с моей точки зрения, является идеальным, но я на данный момент не реализовал его, поэтому в статье речь не о нем.
Решение на базе макросов
Решение, которое мне удалось реализовать с помощью макросов, имеет следующие достоинства:
- Отсутствует нагрузка во время выполнения на доступ к членам структуры.
- Удалось сохранить стандартный синтаксис доступа к членам структуры вида object.member_name или pObject->member_name.
- Нагрузка на память вида O(1). Иными словами, каждый экземпляр структуры с автосравнением занимает столько же места в памяти, сколько и обычная структура. Имеются лишь постоянные (небольшие) затраты памяти на каждый объявляемый тип таких структур.
Из недостатков можно отметить следующие:
- Наличие в структуре дополнительных служебных членов, что снижает удобство от таких инструментов анализа, как Intellisense или DoxyGen.
- Возможность конфликта имен служебных членов с пользовательскими.
- Невозможность инициализации списком инициализаторов вида struct a = {1,2,3}.
Пример использования
Пусть нам нужно создать структуру для хранения данных о людях, эквивалентную следующей обычной структуре:
struct MANPARAMS { std::string name; int age; std::vector<std::string> friend_names; double karma; };
На базе моей библиотеки структура с автоматическими почленными операциями объявляется так:
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); };
После этого, один раз на всю программу, необходимо скомпилировать следующий вызов макроса в одном из *.cpp-файлов:
PARAMFIELD_IMPL(AUTO_MANPARAMS);
Всё! Теперь можно спокойно пользоваться данными структурами как обычно, и сравнивать их на равенство или неравенство, не заботясь о написании соответствующих операторов. Например:
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”); }
Реализация
Как видно из приведенного выше, в начале определения каждой структуры необходимо вызвать макрос PARAMSTRUCT_DECLARE_BEGIN(x), который определит для этой структуры некоторые общие типы и статические служебные члены. После этого нужно при объявлении каждого пользовательского члена вызывать второй макрос, DECLARE_MEMBER_PARAMSTRUCT(type, name), который, помимо объявления собственно члена с указанным именем, определяет служебные члены структуры, связанные с ним.
Основные идеи реализации:
- Для каждого члена структуры автоматически генерируется функция сравнения этого члена.
- Указатели на функции сравнения хранятся в статическом массиве. Оператор сравнения просто перебирает все элементы этого массива и вызывает функцию сравнения для каждого члена.
- При старте программы происходит инициализация этого массива так, чтобы не дублировать код по объявлению членов структуры.
1. Автогенерация функций сравнения каждого члена
Каждая такая функция является членом структуры и производит сравнение «своего» члена данных. Она генерируется в макросе DECLARE_MEMBER_PARAMSTRUCT(type, name) следующим образом:
bool comp##name(const ThisParamFieldClass& a) const \ { \ return name == a.name; \ } \
Где ThisParamFieldClass – это тип нашей структуры, который объявляется через typedef в головном макросе — см. ниже.
2. Массив с указателями на функции сравнения
Головной макрос PARAMSTRUCT_DECLARE_BEGIN(x) объявляет статический массив, в котором хранятся указатели на каждую из функций сравнения членов. Для этого сначала определяется их тип:
#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; \
Здесь же объявляются операторы сравнения:
public: \ bool operator==(const ThisParamFieldClass& a) const; \ bool operator!=(const ThisParamFieldClass& a) const { return !operator==(a); } \
Реализуется оператор сравнения другим макросом (PARAMFIELD_IMPL), однако его реализация является тривиальной при наличии заполненного массива stat_data: нужно всего лишь вызвать функцию сравнения для каждого элемента этого массива.
Для одного лишь сравнения структур нет необходимости хранить в массиве имена членов структуры. Однако хранение имен позволяет расширять концепцию, применяя ее не только к почленному сравнению, но и к другим операциям, например, сохранению и загрузке в текстовом формате, пригодном для чтения человеком.
3. Заполнение данных о членах структуры
Остается решить вопрос с заполнением массива stat_data. Поскольку информация о членах изначально недоступна нигде, кроме макроса DECLARE_MEMBER_PARAMSTRUCT, то заполнять массив возможно только оттуда (прямо или косвенно). Однако этот макрос вызывается внутри объявления структуры, что не самое удобное место для инициализации std::vector. Я решил эту проблему с помощью служебных объектов. Для каждого члена структуры объявляется служебный класс и объект этого класса. Этот класс имеет конструктор — он и добавляет информацию об элементе в статический массив 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;
где populate_statdata – статический флаг, который объявляется в головном макросе и сигнализирует о том, следует ли заполнять массив stat_data именами членов структуры и функциями их сравнения. При старте программы механизм инициализации, описанный ниже, устанавливает populate_statdata=true и создает один экземпляр структуры. При этом конструкторы служебных объектов, связанные с каждым членом структуры, заполняют массив данными о членах. После этого устанавливается populate_statdata=false, и статический массив с информацией о членах больше не изменяется. Данное решение приводит к некоторым потерям времени при каждом создании структуры пользовательской программой, на проверку флага populate_statdata. Однако расход памяти не увеличивается: служебный объект не содержит членов данных, только конструктор.
И наконец, механизм управления флагом populate_statdata: реализуется с помощью статического служебного объекта с конструктором, одного на всю структуру. Этот объект объявляется в головном макросе:
class VcfInitializer \ { \ public: \ VcfInitializer(void); \ }; \ static VcfInitializer vcinit;
Реализация конструктора находится в макросе PARAMFIELD_IMPL(x):
x::VcfInitializer::VcfInitializer(void) \ { \ x::populate_statdata = true; \ ThisParamFieldClass dummy; \ x::populate_statdata = false; \ } \
Полный текст макросов
#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; \ }
Заключение
На базе приведенных выше макросов можно объявлять структуры, для которых автоматически создаются операторы сравнения и другие почленные операции. К другим таким операциям относятся, например, загрузка и сохранение в текстовые файлы, в формат XML. Отсутствие дублирования кода облегчает работу и предохраняет от ошибок. Одно лишь объявление члена структуры добавляет этот член к операциям сравнения, сохранения и загрузки.

