Грязные трюки с макросами C++
В этой статье я хочу сделать две вещи: рассказать, почему макросы — зло и как с этим бороться, а так же продемонстрировать пару используемых мной макросов 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? я нашел довольно полный список причин, заставляющих нас считать макросы злом и некоторые способы от них избавиться. Ниже я приведу этот же список на русском, но примеры и решения проблем будут не совсем такими, как по указанной ссылке. Макросы нельзя отлаживать Во-первых, на самом деле, можно: 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, если это важно),
логику макросов перенести в функции, а сами макросы сделать ответственными только за передачу данных в эти функции,
использовать только макросы, которые не требуют отладки.
При разворачивании макроса могут появиться странные побочные эффекты
Чтобы показать, о каких побочных эффектах идет речь, обычно приводят пример с арифметическими операциями. Я тоже не стану отступать от этой традиции:
#include
скобки вокруг всего выражения скобки вокруг каждого из параметров макроса То есть, вместо #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 () или как-то похоже, проблемы удалось бы избежать.
Решения проблемы: грамотно именовать макросы,
заменить макросы на функции,
не использовать макросы, которые неявно что-либо изменяют.
После всего вышеперечисленного можно дать определение «хорошим» макросам. Хорошие макросы — это макросы, которыене требуют отладки (внутри попросту незачем ставить точку останова)
не имеют побочных эффектов при разворачивании (все обернуто скобочками)
не конфликтуют с именами где-либо (выбран такой вид имен, которые с небольшой долей вероятности будут использованы кем-либо еще)
ничего не изменяют неявно (имя точно отражает, что делает макрос, а вся работа с окружающим кодом, по возможности, ведется только через параметры и «возвращаемое значение»)
Безопасный вызов метода
#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
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
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
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.