Как стать автором
Обновить

Грязные трюки с макросами C++

Время на прочтение 10 мин
Количество просмотров 148K
В этой статье я хочу сделать две вещи: рассказать, почему макросы — зло и как с этим бороться, а так же продемонстрировать пару используемых мной макросов C++, которые упрощают работу с кодом и улучшают его читаемость. Трюки, на самом деле, не такие уж и грязные:
  • Безопасный вызов метода
  • Неиспользуемые переменные
  • Превращение в строку
  • Запятая в аргументе макроса
  • Бесконечный цикл

Заранее предупреждаю: если Вы думаете увидеть под катом что-то крутое, головоломное и сногсшибательное, то ничего такого в статье нет. Статья про светлую сторону макросов.

Несколько полезных ссылок


Для начинающих: статья (на английском) Anders Lindgren — Tips and tricks using the preprocessor (part one), покрывает самые основы макросов.
Для продвинутых: статья (на английском) Anders Lindgren — Tips and tricks using the preprocessor (part two), покрывает более серьезные темы. Кое-что будет и в этой статье, но не все, и с меньшим количеством объяснений.
Для профессионалов: статья (на английском) Aditya Kumar, Andrew Sutton, Bjarne Stroustrup — Rejuvenating C++ Programs through Demacrofication, описывает возможности по замене макросов на фичи C++11.

Небольшое культурное различие


Согласно Википедии и моим собственным ощущениям, в русском языке мы обычно понимаем под словом «макрос» вот это:
#define FUNC(x, y) ((x)^(y))
А следующее:
#define VALUE 1
у нас называется «константой препроцессора» (или попросту «дефайн»'ом). В английском языке немного не так: первое называется function-like macro, а второе — object-like macro (опять же, приведу ссылку на Википедию). То есть, когда они говорят о макросах, они могут иметь в виду как одно, так и другое, так и все вместе. Будьте внимательны при чтении английских текстов.

Что такое хорошо и что такое плохо


В последнее время популярно мнение, что макросы — зло. Мнение это не беспочвенно, но, на мой взгляд, нуждается в пояснениях. В одном из ответов на вопрос Why are preprocessor macros evil and what are the alternatives? я нашел довольно полный список причин, заставляющих нас считать макросы злом и некоторые способы от них избавиться. Ниже я приведу этот же список на русском, но примеры и решения проблем будут не совсем такими, как по указанной ссылке.
  1. Макросы нельзя отлаживать
    Во-первых, на самом деле, можно:
    Go to either project or source file properties by right-clicking and going to «Properties». Under Configuration Properties->C/C++->Preprocessor, set «Generate Preprocessed File» to either with or without line numbers, whichever you prefer. This will show what your macro expands to in context. If you need to debug it on live compiled code, just cut and paste that, and put it in place of your macro while debugging.
    Так что, правильнее будет сказать, что «макросы сложно отлаживать». Но, тем не менее, проблема с отладкой макросов существует.

    Чтобы определить, нуждается ли используемый Вами макрос в отладке, подумайте, есть ли в нем то, ради чего стоит захотеть запихнуть туда точку останова. Это может быть изменение значений, полученных через параметры, объявление переменных, изменение объектов или данных снаружи и тому подобное.
    Решения проблемы:
    • полностью избавиться от макросов, заменив их на функции (можно inline, если это важно),
    • логику макросов перенести в функции, а сами макросы сделать ответственными только за передачу данных в эти функции,
    • использовать только макросы, которые не требуют отладки.
  2. При разворачивании макроса могут появиться странные побочные эффекты
    Чтобы показать, о каких побочных эффектах идет речь, обычно приводят пример с арифметическими операциями. Я тоже не стану отступать от этой традиции:
    #include <iostream>
    #define SUM(a, b) a + b
    int main()
    {
        // Что будет в x?
        int x = SUM(2, 2);
        std::cout << x << std::endl;
        x = 3 * SUM(2, 2);
        std::cout << x << std::endl;
        return 0;
    }
    
    В выводе ожидаем 4 и 12, а получаем 4 и 8. Дело в том, что макрос просто подставляет код туда, куда указано. И в данном случае код будет выглядеть так:
    int x = 3 * 2 + 2;
    
    Это и есть побочный эффект. Чтобы все заработало, как ожидается, нужно изменить наш макрос:
    #include <iostream>
    #define SUM(a, b) (a + b)
    int main()
    {
        // Что будет в x?
        int x = SUM(2, 2);
        std::cout << x << std::endl;
        x = 3 * SUM(2, 2);
        std::cout << x << std::endl;
        return 0;
    }
    
    Теперь верно. Но это еще не все. Перейдем к умножению:
    #define MULT(a, b) a * b
    
    Сразу же запишем его «правильно», но используем чуть иначе:
    #include <iostream>
    #define MULT(a, b) (a * b)
    int main()
    {
        // Что будет в x?
        int x = MULT(2, 2);
        std::cout << x << std::endl;
        x = MULT(3, 2 + 2);
        std::cout << x << std::endl;
        return 0;
    }
    
    Дежавю: снова получаем 4 и 8. В данном случае развернутый макрос будет выглядеть как:
    int x = (3 * 2 + 2);
    
    То есть, теперь нам нужно написать:
    #define MULT(a, b) ((a) * (b))
    
    Используем эту версию макроса и вуаля:
    #include <iostream>
    #define MULT(a, b) ((a) * (b))
    int main()
    {
        // Что будет в x?
        int x = MULT(2, 2);
        std::cout << x << std::endl;
        x = MULT(3, 2 + 2);
        std::cout << x << std::endl;
        return 0;
    }
    
    Теперь все правильно.

    Если абстрагироваться от арифметических операций, то, в общем случае, при написании макросов нам нужны
    • скобки вокруг всего выражения
    • скобки вокруг каждого из параметров макроса
    То есть, вместо
    #define CHOOSE(ifC, chooseA, otherwiseB) ifC ? chooseA : otherwiseB
    
    должно быть
    #define CHOOSE(ifC, chooseA, otherwiseB) ((ifC) ? (chooseA) : (otherwiseB))
    

    Эта проблема усугубляется тем, что далеко не все типы параметров можно обернуть в скобки (реальный пример будет дальше в статье). Из-за этого сделать качественные макросы бывает довольно сложно.

    Кроме того, как напомнил encyclopedist в комментариях, бывают случаи, когда и скобки не спасают:
    В пункте про побочные эффекты вы ещё забыли упомянуть частую проблему — макросы могут вычислять свои аргументы несколько раз. В худшем случае это приводит к странным побочным эффектам, в более легком — к проблемам производительности.
    Пример

    #define SQR(x) ((x) * (x))
    
    y = SQR(x++);
    
    Решения проблемы:
    • отказаться от макросов в пользу функций,
    • использовать макросы с понятным именем, простой реализацией и грамотно расставленными скобками, чтобы программист, использующий такой макрос легко понял, как правильно его использовать.
  3. Макросы не имеют пространства имен
    Если объявлен какой-либо макрос, он не только глобален, но еще и попросту не даст воспользоваться чем-либо с таким же именем (всегда будет подставлена реализация макроса). Самым, наверное, известным примером является проблема с min и max под Windows.
    Решение проблемы — выбирать имена для макросов, которые с низкой вероятностью пересекутся с чем либо, например:
    • имена в UPPERCASE, обычно они могут пересечься только с другими именами макросов,
    • имена с префиксом (имя Вашего проекта, namespace, еще что-то уникальное), пересечение с другими именами будет возможно с очень небольшой вероятностью, но использовать такие макросы за пределами Вашего проекта людям будет немного сложнее.
  4. Макросы могут делать что-то, о чем Вы не подозреваете
    На самом деле, это проблема выбора имени для макроса. Скажем, возьмем тот же пример, который приведен в ответе по ссылке:
    #define begin() x = 0
    #define end() x = 17
    ... a few thousand lines of stuff here ... 
    void dostuff()
    {
        int x = 7;
    
        begin();
    
        ... more code using x ... 
    
        printf("x=%d\n", x);
    
        end();
    
    }
    
    Здесь налицо неверно выбранные имена, которые и вводят в заблуждение. Если бы макросы были названы set0toX() и set17toX() или как-то похоже, проблемы удалось бы избежать.
    Решения проблемы:
    • грамотно именовать макросы,
    • заменить макросы на функции,
    • не использовать макросы, которые неявно что-либо изменяют.

После всего вышеперечисленного можно дать определение «хорошим» макросам. Хорошие макросы — это макросы, которые
  • не требуют отладки (внутри попросту незачем ставить точку останова)
  • не имеют побочных эффектов при разворачивании (все обернуто скобочками)
  • не конфликтуют с именами где-либо (выбран такой вид имен, которые с небольшой долей вероятности будут использованы кем-либо еще)
  • ничего не изменяют неявно (имя точно отражает, что делает макрос, а вся работа с окружающим кодом, по возможности, ведется только через параметры и «возвращаемое значение»)

Безопасный вызов метода


Старая версия, не прошедшая испытания Хабром
#define prefix_safeCall(value, object, method) ((object) ? ((object)->method) : (value))
#define prefix_safeCallVoid(object, method) ((object) ? ((void)((object)->method)) : ((void)(0)))

На самом деле, я использовал вот такую версию
#define prefix_safeCall(defaultValue, objectPointer, methodWithArguments) ((objectPointer) ? ((objectPointer)->methodWithArguments) : (defaultValue))
#define prefix_safeCallVoid(objectPointer, methodWithArguments) ((objectPointer) ? static_cast<void>((objectPointer)->methodWithArguments) : static_cast<void>(0))
Но Хабр — это не IDE, поэтому настолько длинные строки выглядят некрасиво (по крайней мере, на моем мониторе), и я сократил их до удобочитаемого вида.

tenzink в комментариях указал на проблему с этими макросами, которую я благополучно не учел при написании статьи:
prefix_safeCallVoid(getObject(), method());
При таком вызове getObject вызовется дважды.

К сожалению, как показала статья, далеко не каждый программист об этом догадается, поэтому считать эти макросы хорошими я больше не могу. :-(

Тем не менее, похожие макросы (несколько иначе реализованные) я встречал в реальном продакшн коде, они использовались командой программистов, в том числе и мной. Каких либо проблем из-за них на моей памяти не возникало

Новая версия, появившаяся благодаря lemelisk и C++14:
#define prefix_safeCall(defaultValue, objectPointer, methodWithArguments)\
[&](auto&& ptr) -> decltype(auto)\
{\
    return ptr ? (ptr->methodWithArguments) : (defaultValue);\
}\
(objectPointer)

#define prefix_safeCallVoid(objectPointer, methodWithArguments)\
[&](auto&& ptr)\
{\
    if(ptr)\
        (ptr->methodWithArguments); \
}\
(objectPointer)

Версия для C++11
#define prefix_safeCallBaseExpression(defaultValue, objectPointer, methodWithArguments)\
((ptr) ? ((ptr)->methodWithArguments) : (defaultValue))

#define prefix_safeCall(defaultValue, objectPointer, methodWithArguments)\
[&](decltype((objectPointer))&& ptr)\
    -> decltype(prefix_safeCallBaseExpression(defaultValue, ptr, methodWithArguments))\
{\
    return prefix_safeCallBaseExpression(defaultValue, ptr, methodWithArguments);\
}\
(objectPointer)

#define prefix_safeCallVoid(objectPointer, methodWithArguments)\
[&](decltype((objectPointer))&& ptr)\
{\
    if (ptr)\
        (ptr->methodWithArguments);\
}\
(objectPointer)

Обратите внимание на параметр methodWithArguments. Это тот самый пример параметра, который нельзя обернуть скобками. Это значит, что кроме вызова метода в параметр можно запихнуть и что-нибудь еще. Тем не менее, случайно это устроить довольно проблематично, поэтому я не считаю эти макросы «плохими».

Кроме этого, теперь у нас добавился overhead на вызов лямбды. Теоретически, можно предположить, что лямбда, вызываемая там же, где она определена, будет заинлайнена. Но подтверждения этому в сети я не нашел, так что лучше всего будет проверить это «вручную» для Вашего компилятора.

Как эти два макроса используются, думаю, понятно. Если имеем код:
auto somePointer = ...;
if(somePointer)
    somePoiter->callSomeMethod();
то с помощью макроса safeCallVoid он превращается в:
auto somePointer = ...;
prefix_safeCallVoid(somePointer, callSomeMethod());
и, аналогично, для случая с возвращаемым значением:
auto somePointer = ...;
auto x = prefix_safeCall(0, somePointer, callSomeMethod());

Для чего? В первую очередь, эти макросы позволяют увеличить читаемость кода, уменьшить вложенность. Наибольший положительный эффект дают в совокупности с небольшими методами (то есть, если следовать принципам рефакторинга).

Неиспользуемые переменные


#define prefix_unused(variable) ((void)variable)

На самом деле, используемый мной вариант тоже отличается
#define prefix_unused1(variable1) static_cast<void>(variable1)
#define prefix_unused2(variable1, variable2) static_cast<void>(variable1), static_cast<void>(variable2)
#define prefix_unused3(variable1, variable2, variable3) static_cast<void>(variable1), static_cast<void>(variable2), static_cast<void>(variable3)
#define prefix_unused4(variable1, variable2, variable3, variable4) static_cast<void>(variable1), static_cast<void>(variable2), static_cast<void>(variable3), static_cast<void>(variable4)
#define prefix_unused5(variable1, variable2, variable3, variable4, variable5) static_cast<void>(variable1), static_cast<void>(variable2), static_cast<void>(variable3), static_cast<void>(variable4), static_cast<void>(variable5)
Обратите внимание, что, начиная с двух параметров, данный макрос теоретически может обладать побочными эффектами. Для пущей надежности можно воспользоваться классикой:
#define unused2(variable1, variable2)  do {static_cast<void>(variable1); static_cast<void>(variable2);} while(false)
Но, в таком виде он сложнее читаем, из-за чего я использую менее «безопасный» вариант.

Подобный макрос есть, например, в cocos2d-x, там он называется CC_UNUSED_PARAM. Из недостатков: теоретически, он может работать не на всех компиляторах. Тем не менее, в cocos2d-x он для всех платформ определен абсолютно одинаково.

Использование:
int main()
{
    int a = 0; // неиспользуемая переменная.
    prefix_unused(a);
    return 0;
}

Для чего? Этот макрос позволяет избежать предупреждения о неиспользуемой переменной, а читающему код он как бы говорит: «тот кто писал это — знал, что переменная не используется, все в порядке».

Превращение в строку


#define prefix_stringify(something) std::string(#something)

Да, вот так вот сурово, сразу в std::string. Плюсы и минусы использования строкового класса оставим за рамками разговора, поговорим только о макросе.

Использовать его можно так:
std::cout << prefix_stringify("string\n") << std::endl;
И еще так:
std::cout << prefix_stringify(std::cout << prefix_stringify("string\n") << std::endl;) << std::endl;
И даже так:
std::cout << prefix_stringify(#define prefix_stringify(something) std::string(#something)
std::cout << prefix_stringify("string\n") << std::endl;) << std::endl;
Однако, в последнем примере перенос строки будет заменен на пробел. Для реального переноса нужно использовать '\n':
std::cout << prefix_stringify(#define prefix_stringify(something) std::string(#something)\nstd::cout << prefix_stringify("string\n") << std::endl;) << std::endl;
Также, можно использовать и другие символы, например '\' для конкатенации строк, '\t' и прочие.

Для чего? Может использоваться для упрощения вывода отладочной информации или, например, для создания фабрики объектов с текстовыми id (в этом случае, такой макрос может использоваться при регистрации класса в фабрике для превращения имени класса в строку).

Запятая в параметре макроса


#define prefix_singleArgument(...) __VA_ARGS__

Идея подсмотрена здесь.

Пример оттуда же:
#define FOO(type, name) type name
FOO(prefix_singleArgument(std::map<int, int>), map_var);

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

Бесконечный цикл


#define forever() for(;;)

Версия от Джоэла Спольски
#define ever (;;)
for ever { 
   ...
}
P.S. Если кто-нибудь, перейдя по ссылке, не догадался прочитать название вопроса, то звучит оно примерно как «какое худшее реальное злоупотребление макросами Вам встречалось?» ;-)

Использование:
int main()
{
    bool keyPressed = false;
    forever()
    {
        ...
        if(keyPressed)
            break;
    }
    return 0;
}

Для чего? Когда while(true), while(1), for(;;) и прочие стандартные пути создания цикла кажутся не слишком информативными, можно использовать подобный макрос. Едиственный плюс который он дает — чуть лучшую читаемость кода.

Заключение


При правильном использовании макросы вовсе не являются чем-то плохим. Главное, не злоупотреблять ими и следовать нехитрым правилам по созданию «хороших» макросов. И тогда они станут Вашими лучшими помощниками.

Upd. Вернул в статью «Безопасный вызов метода», спасибо lemelisk за подсказку с лямбдами.

P.S.
А какие интересные макросы используете Вы в своих проектах? Не стесняйтесь поделиться в комментариях.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Вы все еще на светлой стороне C++, или уже пошли по темному пути?
28.1% Я — джедай! Я вообще не использую макросы! 281
37.1% Я на светлой стороне, использую только «хорошие» макросы; либо «нехорошие», но проверенные. 371
13.8% Я на темной стороне, и использую самые разные макросы. 138
4.4% Я на темной стороне! А разве в C++ есть что-то еще, кроме макросов? 44
16.6% Я — Император Палпатин! 166
Проголосовали 1000 пользователей. Воздержались 199 пользователей.
Теги:
Хабы:
+36
Комментарии 60
Комментарии Комментарии 60

Публикации

Истории

Работа

Программист C++
122 вакансии
QT разработчик
13 вакансий

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн