Магия макросов для объединения объявления и реализации

    Одна из неприятных проблем — при внесении даже простых изменений приходится править код в нескольких местах. Например, если в класс добавляется поле данных, его нужно добавить в объявление класса, инициализировать в конструкторе(ах), а если переопределены операторы копирования или сравнения — то и в них тоже. Это отнимает время и приводит к ошибкам, если забыть про одно из мест (особенно неприятно забыть инициализировать, такие ошибки могут жить годами, и вызывать неожиданные, трудновоспроизводимые проблемы).

    Обычно, если класс предполагает активную модификацию полей, то пишется макрос, который берёт реализацию действий с полем (инициализацию, копирование, сериализацию, reflection) на себя.
    В результате переменная должна прописываться всего в двух местах — объявляться в классе и реализовываться (или регистрироваться для последующего использования в реализации).

    Получается что-то вроде:
    class TData
    {
    public:
    	int Number;
    	float Factor;
    
    	BEGIN_FIELDS
    		FIELD(Number, 0)
    		FIELD(Factor, 1.0f)
    	END_FIELDS
    };
    

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

    Ещё один пример, попроще. Нужно сделать отражение перечисления, например, сопоставить варианту перечисления строку его имени. Обычно это делается как-то так:
    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)
    }
    

    Объявление TModelTypeReflection и реализацию Register предоставлю воображению читателя.

    Довольно долго я довольствовался таким положением дел. Но недавно подумал, что можно сделать и получше, обойдясь единственным объявлением. Сделать это можно с помощью всё тех же макросов.

    Для последнего примера это будет выглядеть так:
    #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)))
    }
    

    Макросы DECLARE_MODEL_TYPE развернутся сначала в элементы перечисления, затем код из END_MODEL_TYPE закроет блок перечисления и вставит заголовок функции, дальше в тело функции вставятся вызовы Register для элементов, только в обратном порядке, и наконец фигурная скобка закроет блок функции (поэтому она и без точки с запятой).
    Похожий код можно написать и для полей класса.

    Осталось только сказать о недостатках:
    • регистрация в обратном порядке, но если порядок всё же важен, можно это учесть в реализации Register, например, добавлять очередное поле в начало списка, а не в конец;
    • проблемы с системами автоматической генерацией документации вроде DOxygen, они не догадаются развернуть макросы;
    • необходимость при добавлении поля добавлять ещё одну закрывающую скобку после END_MODEL_TYPE. Неприятный недостаток, т.к. править код приходится всё равно в двух местах. Немного радует, что забыть о скобке не позволит препроцессор.

    Альтернативное решение, позволяющее объединить объявление с реализацией — использование кодогенератора, но и этот подход тоже имеет свои недостатки.
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 26

      +3
      Неделя макросного безумства.
        +2
        Вам правда это удобно?..
          0
          По моему опыту (меня тоже время от времени пробивает на подобные макросы) — это удобно до тех пор, пока с проектом идёт активная работа. Стоит отложить проект на несколько месяцев — и некоторые макросы потом начинают доставлять неудобства — приходится постоянно заглядывать в определения. Всё хорошо в меру.
            0
            От подобных проблем спасает волшебная комбинация клавиш в IDE, которая раскрывает весь препроцессор в выделенном коде.

            Дойдут руки, думаю написать, как это сделать для Visual Studio. По сути для этого нужно написать небольшой макрос на VBA, который вытаскивает из нужного проекта настройки компиляции (дефайнов и инклудов) и вызывает специальную утилиту, которая, в свою очередь, вызывает компилятор с опцией «preprocess to file». Затем из полученного файла вытаскивается нужный кусок и отображается пользователю.

            Например. Имеем такой код:
            DEFINE_ENUM_WITH_DEFAULT_VALUE( EHumanizationMode, eCppCode,
                ( ( eDisabled ) )
                ( ( eCppCode ) )
                ( ( eAutoexpCode ) ) );

            Выделяем, нажимем кнопочку, привязанную к VBA макросу, немножко ждем и видем на экране:
            //
            // CodeFactory: BEGIN Auto generated preprocessed code
            //
            class EHumanizationMode : public ISmartEnumBase
            {
                private: template< class T > operator T() const;
                template< class T >
                operator T();
                public: enum EImpl
                {
                    eDisabled, eCppCode, eAutoexpCode
                };
                private: EImpl m_eValue;
                public: inline EHumanizationMode( EImpl m_eValue ) : m_eValue( m_eValue )
                {
                }
                public: inline bool operator==( const EHumanizationMode &rEnum ) const
                {
                    return m_eValue == rEnum.m_eValue;
                }
                inline bool operator==( const EImpl oValue ) const
                {
                    return m_eValue == oValue;
                }
                template< class T >
                inline bool operator!=( const T &rValue ) const
                {
                    return !( *this == rValue );
                }
                public: inline operator EImpl() const
                {
                    return m_eValue;
                }
                inline operator EImpl&()
                {
                    return m_eValue;
                }
                public: inline int ToInt() const
                {
                    return reinterpret_cast< const int & >( m_eValue );
                }
                inline int &ToInt()
                {
                    return reinterpret_cast< int & >( m_eValue );
                }
                const char *ToString() const
                {
                    switch( m_eValue )
                    {
                        case EHumanizationMode::eDisabled : return "EHumanizationMode::eDisabled";
                        case EHumanizationMode::eCppCode : return "EHumanizationMode::eCppCode";
                        case EHumanizationMode::eAutoexpCode : return "EHumanizationMode::eAutoexpCode";
                        default: ((void)0);
                        abort();
                        return "Error";
                    };
                }
                static EHumanizationMode FromInt( int eValue )
                {
                    ((void)0);
                    EHumanizationMode oRes;
                    oRes.ToInt() = eValue;
                    return oRes;
                }
                static bool ValidateValue( const int eValue )
                {
                    switch( eValue )
                    {
                        case EHumanizationMode::eDisabled : case EHumanizationMode::eCppCode : case EHumanizationMode::eAutoexpCode : return true;
                        default: return false;
                    };
                }
                public: inline EHumanizationMode() : m_eValue( eCppCode )
                {
                }
            };;
            //
            // CodeFactory: END Auto generated preprocessed code
            //

            Сейчас есть небольшие проблемы с форматированием результата, но они решаемые…
            0
            Удобно — не то слово, ещё и безопасно. Описанная в начале ситуация с забыванием добавить работу с полем в одном из мест — это не для красного словца, а несколько раз со мной случалась. Так что речь не столько об удобстве, сколько о желании избежать таких проблем в дальнейшем.
              0
              Я делаю генерацию подобного кода отдельным скриптом.
              Во-первых, получается нормальный синтаксис в основном коде, что все-таки удобнее чем такие вот макросы.
              Во-вторых, возможности скриптовых языков намного больше чем у макросов С.

              Но есть и недостатки (для меня несущественные) — специальная организация проекта, чтобы сгенерированный код не путался в контроле версий, а создавался в момент сборки.
                0
                Вот это
                #define END_MODEL_TYPE					\
                };	void TModelTypeReflection::RegisterTypes()	{
                
                
                безопасно?

                Ну пусть в вашем случае проблемы не будет. А если у нас есть, например,
                #define START_ME <some code here>
                #define END_ME }; someOtherCode {
                struct MyStruct
                {
                START_ME
                <...>
                END_ME
                }
                

                На первый взгляд все нормально. Потом человек, который не знает, что это за макрос (ну забыл он посмотреть, или просто не там поглядел, или еще что-нибудь) дописывает еще одно поле:
                struct MyStruct
                {
                START_ME
                <...>
                END_ME
                int iWasAddedLater;
                }
                

                На вид — все здорово, какой-то макрос начал какой-то блок, потом закончил его. Все прекрасно. А на деле — отнюдь. На мой взгляд (на объективность не претендую), такая ситуация ничуть не менее вероятно чем «забыл инициализировать новое поле».
                  0
                  Думаю ничего страшного не случится. Сначала компилятор поругается ворнингом на неиспользуемую переменную, потом сильно поругается на код, который к этому полю обращается (не просто же так это поле было добавлено), ибо такого поля не окажется.
                0
                Поддерживаю автора. Подобные вещи иногда значительно облегчают жизнь С++ программиста.
                При переходе на C# остро страдаю от отсутствия подобных инструментов! приходится осваивать кодогенерацию…
                +1
                А я уж думал, это про лисп статья
                  0
                  В rss категория не показывается :)
                  +1
                  Альтернативное решение, позволяющее объединить объявление с реализацией — использование кодогенератора, но и этот подход тоже имеет свои недостатки.


                  Мне как раз кодогенерация, при всех её недостатках, кажется более удобным решением. Особенно перспективным кажется кодогенерация макросов и последующее их использование.

                    +1
                    Почему фундаментальные принципы (избегать дублирования) нарушаются в самом синтаксисе языка?
                      0
                      <ирония>Видимо, потому что язык появился раньше чем фундаментальные принципы и потому является более фундаментальным.

                      *интересно что будет, если забыть закрыть тег иронии??
                        +1
                        Возникнет антиирония и аннигиляция :)
                      0
                      последнее можно сделать через TypeList и шаблон с рекурсивным вызовом функции регистрации
                        +1
                          +1
                          код разворачивается в 4 последовательных вызова
                          goo.gl/Nrqtl
                            0
                            Конечно можно, но целью было оставить одно единственное место, куда нужно добавлять новый элемент перечисления, в приведённом коде их по-прежнему два (одно в enum, второе в typelist).
                              0
                              да, я понял.
                              если элементами тайплиста будут классы, то их идентификаторы можно получать из тайплиста, что оставит только одно объявление(я недавно в своем блоге о подобном писал pushkoff.blogspot.com/2012/04/blog-post.html). с интами этот номер не прошел.
                          0
                          По заголовку подумалось, что статья об объединении .h и .cpp.
                            +1
                            То, что вы описали с перечислением, называется техникой X-macro.
                            www.drdobbs.com/cpp/the-x-macro/228700289
                              0
                              Цель одна — не дублировать код, но техники — совсем разные.
                              0
                              Предпочитаю избегать переопределения конструкторов копирования и операторов присвоения в пользу использования всякого рода врапперов и умных указателей.

                              Я заметил, что как правило оператор присвоения переопределяется из-за одного-двух мемберов, а остальным, допустим десяти, отлично подходит и стандартный оператор присвоения. В таком случае можно завернуть эти два несчастных мембера в структуру с переопределенным оператором присвоения, а основной класс не трогать.
                                0
                                Многие люди считают, что умные указатели привносятт много накладных расходов, в следствии чего отказываются от их использования…
                                  0
                                  Многие люди не проверяют свои догадки измерениями и занимаются преждевременной оптимизацией и при этом неоправданно усложняют код программ.

                              Only users with full accounts can post comments. Log in, please.