- Неиспользуемые переменные
- Превращение в строку
- Запятая в аргументе макроса
- Бесконечный цикл
Заранее предупреждаю: если Вы думаете увидеть под катом что-то крутое, головоломное и сногсшибательное, то ничего такого в статье нет. Статья про светлую сторону макросов.
Несколько полезных ссылок
Для начинающих: статья (на английском) 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? я нашел довольно полный список причин, заставляющих нас считать макросы злом и некоторые способы от них избавиться. Ниже я приведу этот же список на русском, но примеры и решения проблем будут не совсем такими, как по указанной ссылке.
-
Макросы нельзя отлаживатьВо-первых, на самом деле, можно:
Так что, правильнее будет сказать, что «макросы сложно отлаживать». Но, тем не менее, проблема с отладкой макросов существует.
Чтобы определить, нуждается ли используемый Вами макрос в отладке, подумайте, есть ли в нем то, ради чего стоит захотеть запихнуть туда точку останова. Это может быть изменение значений, полученных через параметры, объявление переменных, изменение объектов или данных снаружи и тому подобное.
Решения проблемы:
- полностью избавиться от макросов, заменив их на функции (можно inline, если это важно),
- логику макросов перенести в функции, а сами макросы сделать ответственными только за передачу данных в эти функции,
- использовать только макросы, которые не требуют отладки.
-
При разворачивании макроса могут появиться странные побочные эффектыЧтобы показать, о каких побочных эффектах идет речь, обычно приводят пример с арифметическими операциями. Я тоже не стану отступать от этой традиции:
#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++);
Решения проблемы:
- отказаться от макросов в пользу функций,
- использовать макросы с понятным именем, простой реализацией и грамотно расставленными скобками, чтобы программист, использующий такой макрос легко понял, как правильно его использовать.
-
Макросы не имеют пространства именЕсли объявлен какой-либо макрос, он не только глобален, но еще и попросту не даст воспользоваться чем-либо с таким же именем (всегда будет подставлена реализация макроса). Самым, наверное, известным примером является проблема с min и max под Windows.
Решение проблемы — выбирать имена для макросов, которые с низкой вероятностью пересекутся с чем либо, например:
- имена в UPPERCASE, обычно они могут пересечься только с другими именами макросов,
- имена с префиксом (имя Вашего проекта, namespace, еще что-то уникальное), пересечение с другими именами будет возможно с очень небольшой вероятностью, но использовать такие макросы за пределами Вашего проекта людям будет немного сложнее.
-
Макросы могут делать что-то, о чем Вы не подозреваетеНа самом деле, это проблема выбора имени для макроса. Скажем, возьмем тот же пример, который приведен в ответе по ссылке:
#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() или как-то похоже, проблемы удалось бы избежать.
Решения проблемы:
- грамотно именовать макросы,
- заменить макросы на функции,
- не использовать макросы, которые неявно что-либо изменяют.
После всего вышеперечисленного можно дать определение «хорошим» макросам. Хорошие макросы — это макросы, которые
- не требуют отладки (внутри попросту незачем ставить точку останова)
- не имеют побочных эффектов при разворачивании (все обернуто скобочками)
- не конфликтуют с именами где-либо (выбран такой вид имен, которые с небольшой долей вероятности будут использованы кем-либо еще)
- ничего не изменяют неявно (имя точно отражает, что делает макрос, а вся работа с окружающим кодом, по возможности, ведется только через параметры и «возвращаемое значение»)
Безопасный вызов метода
tenzink в комментариях указал на проблему с этими макросами, о которой я уже и забыл думать:
prefix_safeCallVoid(getObject(), method());
При таком вызове getObject вызовется дважды.
К сожалению, как показала статья, далеко не каждый программист об этом догадается, поэтому считать эти макросы хорошими я больше не могу. 🙁
#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, поэтому настолько длинные строки выглядят некрасиво (по крайней мере, на моем мониторе), и я сократил их до удобочитаемого вида.
Обратите внимание на параметр method. Это тот самый пример параметра, который нельзя обернуть скобками. Это значит, что кроме вызова метода в параметр можно запихнуть и что-нибудь еще. Тем не менее, случайно это устроить довольно проблематично, поэтому я не считаю эти макросы «плохими». (Но теперь считаю по другой причине)
Тем не менее, похожие макросы (несколько иначе реализованные) я встречал в реальном продакшн коде, они использовались командой программистов, в том числе и мной. Каких либо проблем из-за них на моей памяти не возникало
Как эти два макроса используются, думаю, понятно. Если имеем код:
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(;;) и прочие стандартные пути создания цикла кажутся не слишком информативными, можно использовать подобный макрос. Едиственный плюс который он дает — чуть лучшую читаемость кода.
Заключение
При правильном использовании макросы вовсе не являются чем-то плохим. Главное, не злоупотреблять ими и следовать нехитрым правилам по созданию «хороших» макросов. И тогда они станут Вашими лучшими помощниками.
P.S.
А какие интересные макросы используете Вы в своих проектах? Не стесняйтесь поделиться в комментариях.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
ссылка на оригинал статьи http://habrahabr.ru/post/246971/
Добавить комментарий