- PVSM.RU - https://www.pvsm.ru -
В этой статье я хочу сделать две вещи: рассказать, почему макросы — зло и как с этим бороться, а так же продемонстрировать пару используемых мной макросов C++, которые упрощают работу с кодом и улучшают его читаемость. Трюки, на самом деле, не такие уж и грязные:
Заранее предупреждаю: если Вы думаете увидеть под катом что-то крутое, головоломное и сногсшибательное, то ничего такого в статье нет. Статья про светлую сторону макросов.
Для начинающих: статья (на английском) Anders Lindgren — Tips and tricks using the preprocessor (part one) [1], покрывает самые основы макросов.
Для продвинутых: статья (на английском) Anders Lindgren — Tips and tricks using the preprocessor (part two) [2], покрывает более серьезные темы. Кое-что будет и в этой статье, но не все, и с меньшим количеством объяснений.
Для профессионалов: статья (на английском) Aditya Kumar, Andrew Sutton, Bjarne Stroustrup — Rejuvenating C++ Programs through Demacrofication [3], описывает возможности по замене макросов на фичи C++11.
Согласно Википедии [4] и моим собственным ощущениям, в русском языке мы обычно понимаем под словом «макрос» вот это:
#define FUNC(x, y) ((x)^(y))
А следующее:
#define VALUE 1
у нас называется «константой препроцессора» (или попросту «дефайн»'ом). В английском языке немного не так: первое называется function-like macro, а второе — object-like macro (опять же, приведу ссылку на Википедию [5]). То есть, когда они говорят о макросах, они могут иметь в виду как одно, так и другое, так и все вместе. Будьте внимательны при чтении английских текстов.
В последнее время популярно мнение, что макросы — зло. Мнение это не беспочвенно, но, на мой взгляд, нуждается в пояснениях. В одном из ответов [6] на вопрос Why are preprocessor macros evil and what are the alternatives? [7] я нашел довольно полный список причин, заставляющих нас считать макросы злом и некоторые способы от них избавиться. Ниже я приведу этот же список на русском, но примеры и решения проблем будут не совсем такими, как по указанной ссылке.
Так что, правильнее будет сказать, что «макросы сложно отлаживать». Но, тем не менее, проблема с отладкой макросов существует.
Чтобы определить, нуждается ли используемый Вами макрос в отладке, подумайте, есть ли в нем то, ради чего стоит захотеть запихнуть туда точку останова. Это может быть изменение значений, полученных через параметры, объявление переменных, изменение объектов или данных снаружи и тому подобное.
Решения проблемы:
#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))
Эта проблема усугубляется тем, что далеко не все типы параметров можно обернуть в скобки (реальный пример будет дальше в статье). Из-за этого сделать качественные макросы бывает довольно сложно.
Решения проблемы:
Решение проблемы — выбирать имена для макросов, которые с низкой вероятностью пересекутся с чем либо, например:
#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=%dn", 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, поэтому настолько длинные строки выглядят некрасиво (по крайней мере, на моем мониторе), и я сократил их до удобочитаемого вида.
Обратите внимание на параметр 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("stringn") << std::endl;
И еще так:
std::cout << prefix_stringify(std::cout << prefix_stringify("stringn") << std::endl;) << std::endl;
И даже так:
std::cout << prefix_stringify(#define prefix_stringify(something) std::string(#something)
std::cout << prefix_stringify("stringn") << std::endl;) << std::endl;
Однако, в последнем примере перенос строки будет заменен на пробел. Для реального переноса нужно использовать 'n':
std::cout << prefix_stringify(#define prefix_stringify(something) std::string(#something)nstd::cout << prefix_stringify("stringn") << std::endl;) << std::endl;
Также, можно использовать и другие символы, например '' для конкатенации строк, 't' и прочие.
Для чего? Может использоваться для упрощения вывода отладочной информации или, например, для создания фабрики объектов с текстовыми id (в этом случае, такой макрос может использоваться при регистрации класса в фабрике для превращения имени класса в строку).
#define prefix_singleArgument(...) __VA_ARGS__
Идея подсмотрена здесь [10].
Пример оттуда же:
#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.
А какие интересные макросы используете Вы в своих проектах? Не стесняйтесь поделиться в комментариях.
Автор: marked-one
Источник [11]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/razrabotka/78340
Ссылки в тексте:
[1] Anders Lindgren — Tips and tricks using the preprocessor (part one): http://www.iar.com/Global/Resources/Developers_Toolbox/C_Cplusplus_Programming/Tips%20and%20tricks%20using%20the%20preprocessor%20%28part%20one%29.pdf
[2] Anders Lindgren — Tips and tricks using the preprocessor (part two): http://www.iar.com/Global/Resources/Developers_Toolbox/C_Cplusplus_Programming/Tips%20and%20tricks%20using%20the%20preprocessor%20%28part%20two%29.pdf
[3] Aditya Kumar, Andrew Sutton, Bjarne Stroustrup — Rejuvenating C++ Programs through Demacrofication: http://www.stroustrup.com/icsm-2012-demacro.pdf
[4] Википедии: https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B5%D0%BF%D1%80%D0%BE%D1%86%D0%B5%D1%81%D1%81%D0%BE%D1%80_%D0%A1%D0%B8#.D0.9A.D0.BE.D0.BD.D1.81.D1.82.D0.B0.D0.BD.D1.82.D1.8B_.D0.B8_.D0.BC.D0.B0.D0.BA.D1.80.D0.BE.D1.81.D1.8B_.23define
[5] Википедию: https://en.wikipedia.org/wiki/C_preprocessor#Macro_definition_and_expansion
[6] В одном из ответов: http://stackoverflow.com/a/14041847
[7] Why are preprocessor macros evil and what are the alternatives?: http://stackoverflow.com/questions/14041453/why-are-preprocessor-macros-evil-and-what-are-the-alternatives
[8] 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.: http://stackoverflow.com/a/1391012
[9] проблема с min и max под Windows: http://easy-coding.blogspot.ru/2009/02/stdmin-stdmax-visual-studio.html
[10] Идея подсмотрена здесь: http://stackoverflow.com/a/13842612
[11] Источник: http://habrahabr.ru/post/246971/
Нажмите здесь для печати.