Автоматическая генерация операторов сравнения структур в C++

в 0:44, , рубрики: c++, макросы, Программирование, метки: , ,

Язык C++ для всех пользовательских классов и структур генерирует по умолчанию копирующий конструктор и копирующий оператор присваивания. Тем самым для важного ряда случаев программист освобождается от написания указанных функций вручную. Например, операторы по умолчанию хорошо работают для структур, которые содержат данные. При этом данные могут храниться как в простых типах, так и в сложных контейнерах, таких как std::vector или std::string.

В свете этого удобно было бы иметь и операторы сравнения структур == и != по умолчанию, однако компилятор C++, в соответствии со стандартом, не генерирует их.

Написать оператор почленного сравнения структур несложно, однако такая организация программы является неудобной и опасной с точки зрения ошибок. Скажем, если программист добавит новый член в структуру, но забудет добавить соответствующее сравнение в пользовательском операторе сравнения — то в программе образуется довольно трудно диагностируемая ошибка. Тем более что объявление структуры и пользовательский оператор ее сравнения обычно разнесены друг от друга, так как находятся в разных файлах (*.h и *.cpp).

Автоматизировать написание операторов почленного сравнения в рамках C++ непросто, так как в этом языке отсутствуют средства, позволяющие во время работы программы выяснить, сколько и каких членов содержится в структуре.

В середине 2000х годов, работая над крупным проектом, который постоянно эволюционировал и требовал частых изменений структур данных, я задался целью решить вопрос операторов сравнения раз и навсегда. В результате была создана конструкция на C++ с применением макросов, позволяющая объявлять структуры с последующей автоматической генерацией операторов почленного сравнения их. Эта же конструкция позволила автоматически реализовать и другие почленные операции: загрузка и сохранение данных в файлы. Предлагаю ее вашему вниманию.

Другие существующие решения

На данный момент мне известны следующие альтернативные решения описанной проблемы:

  1. Использование динамических структур. Вместо обычной структуры C++ применяется контейнер разнородных элементов, которые приведены к единому типу. Например, типу VARIANT из Windows OLE. Также используется контейнер строк для хранения имен членов. Тем самым имена членов, их типы и количество делаются доступными программе во время выполнения. Однако такой подход приводит к затратам во время выполнения программы на доступ к членам такой структуры. Синтаксис доступа вида object.member_name или pObject->member_name становится недоступным, его приходится менять на что-нибудь вроде object.at(“member_name”). Кроме того, имеется линейный рост потребления памяти: каждый экземпяр структуры занимает больше места в памяти, чем обычная (статическая) структура.
  2. Использование библиотеки boost, а именно, контейнера boost::fusion::map. Здесь удалось все затраты взвалить на плечи компилятора, однако традиционный синтаксис доступа к членам сохранить не удалось. Приходится пользоваться конструкциями вида: at_key<member_name>(object).
  3. Генерация кода 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. Отсутствие дублирования кода облегчает работу и предохраняет от ошибок. Одно лишь объявление члена структуры добавляет этот член к операциям сравнения, сохранения и загрузки.

Автор: MichaelBorisov


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


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