Автоматическая генерация операторов сравнения структур в C++Источник: habrahabr MichaelBorisov
Язык C++ для всех пользовательских классов и структур генерирует по умолчанию копирующий конструктор и копирующий оператор присваивания. Тем самым для важного ряда случаев программист освобождается от написания указанных функций вручную. Например, операторы по умолчанию хорошо работают для структур, которые содержат данные. При этом данные могут храниться как в простых типах, так и в сложных контейнерах, таких как std::vector или std::string. В свете этого удобно было бы иметь и операторы сравнения структур == и != по умолчанию, однако компилятор C++, в соответствии со стандартом, не генерирует их. Автоматизировать написание операторов почленного сравнения в рамках C++ непросто, так как в этом языке отсутствуют средства, позволяющие во время работы программы выяснить, сколько и каких членов содержится в структуре. В середине 2000х годов, работая над крупным проектом, который постоянно эволюционировал и требовал частых изменений структур данных, я задался целью решить вопрос операторов сравнения раз и навсегда. В результате была создана конструкция на C++ с применением макросов, позволяющая объявлять структуры с последующей автоматической генерацией операторов почленного сравнения их. Эта же конструкция позволила автоматически реализовать и другие почленные операции: загрузка и сохранение данных в файлы. Предлагаю ее вашему вниманию. Другие существующие решенияНа данный момент мне известны следующие альтернативные решения описанной проблемы:
Решение на базе макросовРешение, которое мне удалось реализовать с помощью макросов, имеет следующие достоинства:
Из недостатков можно отметить следующие:
Пример использованияПусть нам нужно создать структуру для хранения данных о людях, эквивалентную следующей обычной структуре:
На базе моей библиотеки структура с автоматическими почленными операциями объявляется так:
После этого, один раз на всю программу, необходимо скомпилировать следующий вызов макроса в одном из *.cpp-файлов:
Всё! Теперь можно спокойно пользоваться данными структурами как обычно, и сравнивать их на равенство или неравенство, не заботясь о написании соответствующих операторов. Например:
РеализацияКак видно из приведенного выше, в начале определения каждой структуры необходимо вызвать макрос PARAMSTRUCT_DECLARE_BEGIN(x), который определит для этой структуры некоторые общие типы и статические служебные члены. После этого нужно при объявлении каждого пользовательского члена вызывать второй макрос, DECLARE_MEMBER_PARAMSTRUCT(type, name), который, помимо объявления собственно члена с указанным именем, определяет служебные члены структуры, связанные с ним. Основные идеи реализации:
1. Автогенерация функций сравнения каждого членаКаждая такая функция является членом структуры и производит сравнение "своего" члена данных. Она генерируется в макросе DECLARE_MEMBER_PARAMSTRUCT(type, name) следующим образом:
Где ThisParamFieldClass - это тип нашей структуры, который объявляется через typedef в головном макросе - см. ниже.
2. Массив с указателями на функции сравненияГоловной макрос PARAMSTRUCT_DECLARE_BEGIN(x) объявляет статический массив, в котором хранятся указатели на каждую из функций сравнения членов. Для этого сначала определяется их тип:
А затем объявляется массив:
Здесь же объявляются операторы сравнения:
Реализуется оператор сравнения другим макросом (PARAMFIELD_IMPL), однако его реализация является тривиальной при наличии заполненного массива stat_data: нужно всего лишь вызвать функцию сравнения для каждого элемента этого массива. Для одного лишь сравнения структур нет необходимости хранить в массиве имена членов структуры. Однако хранение имен позволяет расширять концепцию, применяя ее не только к почленному сравнению, но и к другим операциям, например, сохранению и загрузке в текстовом формате, пригодном для чтения человеком.
3. Заполнение данных о членах структурыОстается решить вопрос с заполнением массива stat_data. Поскольку информация о членах изначально недоступна нигде, кроме макроса DECLARE_MEMBER_PARAMSTRUCT, то заполнять массив возможно только оттуда (прямо или косвенно). Однако этот макрос вызывается внутри объявления структуры, что не самое удобное место для инициализации std::vector. Я решил эту проблему с помощью служебных объектов. Для каждого члена структуры объявляется служебный класс и объект этого класса. Этот класс имеет конструктор - он и добавляет информацию об элементе в статический массив stat_data:
где populate_statdata - статический флаг, который объявляется в головном макросе и сигнализирует о том, следует ли заполнять массив stat_data именами членов структуры и функциями их сравнения. При старте программы механизм инициализации, описанный ниже, устанавливает populate_statdata=true и создает один экземпляр структуры. При этом конструкторы служебных объектов, связанные с каждым членом структуры, заполняют массив данными о членах. После этого устанавливается populate_statdata=false, и статический массив с информацией о членах больше не изменяется. Данное решение приводит к некоторым потерям времени при каждом создании структуры пользовательской программой, на проверку флага populate_statdata. Однако расход памяти не увеличивается: служебный объект не содержит членов данных, только конструктор. И наконец, механизм управления флагом populate_statdata: реализуется с помощью статического служебного объекта с конструктором, одного на всю структуру. Этот объект объявляется в головном макросе:
Реализация конструктора находится в макросе PARAMFIELD_IMPL(x):
Полный текст макросов
ЗаключениеНа базе приведенных выше макросов можно объявлять структуры, для которых автоматически создаются операторы сравнения и другие почленные операции. К другим таким операциям относятся, например, загрузка и сохранение в текстовые файлы, в формат XML. Отсутствие дублирования кода облегчает работу и предохраняет от ошибок. Одно лишь объявление члена структуры добавляет этот член к операциям сравнения, сохранения и загрузки. |