Во многих макросах C / C++ я вижу код макроса, заключенный в бессмысленный цикл do while. Вот примеры.
#define FOO(X) do { f(X); g(X); } while (0)
#define FOO(X) if (1) { f(X); g(X); } else
Я не вижу, что делает do while. Почему бы просто не написать это без него?
#define FOO(X) f(X); g(X)
Напоминаем, что конструкция do while несовместима с операторами return, поэтому конструкция if (1) { ... } else ((void)0) имеет более совместимые применения в стандарте C. А в GNU C вы предпочтете конструкцию, описанную в моем ответе.





do ... while и if ... else созданы для того, чтобы
точка с запятой после макроса всегда означает одно и то же. Скажем ты
было что-то вроде вашего второго макроса.
#define BAR(X) f(x); g(x)
Если бы вы использовали BAR(X); в операторе if ... else, где тела оператора if не были заключены в фигурные скобки, вы бы получили неприятный сюрприз.
if (corge)
BAR(corge);
else
gralt();
Приведенный выше код расширится на
if (corge)
f(corge); g(corge);
else
gralt();
что синтаксически неверно, так как else больше не связано с if. Заключение в фигурные скобки внутри макроса не помогает, поскольку точка с запятой после фигурных скобок синтаксически неверна.
if (corge)
{f(corge); g(corge);};
else
gralt();
Есть два способа решить проблему. Первый - использовать запятую для упорядочивания операторов внутри макроса, не лишая его способности действовать как выражение.
#define BAR(X) f(X), g(X)
Вышеупомянутая версия bar BAR расширяет приведенный выше код до следующего, что является синтаксически правильным.
if (corge)
f(corge), g(corge);
else
gralt();
Это не сработает, если вместо f(X) у вас есть более сложный код, который должен быть помещен в отдельный блок, например, для объявления локальных переменных. В наиболее общем случае решение состоит в том, чтобы использовать что-то вроде do ... while, чтобы макрос был единственным оператором, который без путаницы принимает точку с запятой.
#define BAR(X) do { \
int i = f(X); \
if (i > 4) g(i); \
} while (0)
Вам не обязательно использовать do ... while, вы также можете что-то приготовить с if ... else, хотя, когда if ... else расширяется внутри if ... else, это приводит к "висящий еще", что может затруднить обнаружение существующей проблемы с висячим else, как в следующий код.
if (corge)
if (1) { f(corge); g(corge); } else;
else
gralt();
Дело в том, чтобы использовать точку с запятой в контекстах, где висящая точка с запятой ошибочна. Конечно, здесь можно (и, вероятно, следует) возразить, что было бы лучше объявить BAR как фактическую функцию, а не как макрос.
Таким образом, do ... while призван обойти недостатки препроцессора C. Когда эти руководства по стилю C говорят вам отказаться от препроцессора C, это именно то, о чем они беспокоятся.
В моем собственном ответе пропущена «проблема else, если фигурные скобки и точка с запятой», но вы забыли проблему области видимости, а также оптимизацию компилятора do / while (false) ... :-p ...
Но почему бы просто не #define FOO (X) {f (X); г (Х); }?
@korona: if (выражение) FOO (X); else ... такой код будет недопустимым, потому что у вас есть 2 оператора в предложении if.
Разве это не веский аргумент в пользу того, чтобы всегда использовать фигурные скобки в операторах if, while и for? Если вы всегда делаете это (как это требуется, например, для MISRA-C), описанная выше проблема исчезнет.
Пример запятой должен быть #define BAR(X) (f(X), g(X)), иначе приоритет оператора может испортить семантику.
Разве вы не можете избежать проблемы с болтающимся остальным, используя #define FOO(x) if (1) { ... } else (void)0?
Как сказал @SteveMelnikoff, каждая упомянутая проблема должна быть решена очень простым исправлением - добавлением скобок в каждую ветвь операторов if / while / for, даже если они содержат только один оператор. Я в основном программист PHP, но всегда везде использую фигурные скобки, даже у меня таких проблем нет :) Использовать скобки везде должно быть проще, чем использовать такие конструкции do/while(0), или я ошибаюсь?
@DawidFerenczy: хотя и ты, и я - четыре с половиной года назад - хорошо замечаем, мы должны жить в реальном мире. Если мы не можем гарантировать, что все операторы if и т. д. В нашем коде используют фигурные скобки, то упаковка макросов, подобная этой, - простой способ избежать проблем.
А что насчет этой простой конструкции: ({})? Смотрите мой ответ.
Примечание: форма if (1) {...} else void(0) более безопасна, чем do {...} while(0) для макросов, параметры которых представляют собой код, включенный в раскрытие макроса, поскольку она не изменяет поведение ключевых слов break или continue. Например: for (int i = 0; i < max; ++i) { MYMACRO( SomeFunc(i)==true, {break;} ) } вызывает неожиданное поведение, когда MYMACRO определен как #define MYMACRO(X, CODE) do { if (X) { cout << #X << endl; {CODE}; } } while (0), потому что разрыв влияет на цикл while макроса, а не на цикл for в месте вызова макроса.
@ChrisKline void(0) недопустимый код C. Кроме того, он по-прежнему не решает проблему зависания else.
@ace void(0) был опечаткой, я имел в виду (void)0. И я считаю, что этот делает решает проблему "висячего другого": обратите внимание, что после (void)0 нет точки с запятой. В этом случае свисание else (например, if (cond) if (1) foo() else (void)0 else { /* dangling else body */ }) вызывает ошибку компиляции. Вот живой пример, демонстрирующий это
Проблема не в том, что вы не можете ставить точку с запятой после блока, а в том, что эта точка с запятой является отдельным оператором, поэтому else все еще синтаксически неверен.
@ChrisKline MYMACRO( SomeFunc(i)==true, {break;} ) - плохой код. Попытка решить все возможные проблемы с помощью макросов делает макросы слишком сложными без реальных преимуществ.
@ jfm3 - Хороший ответ на вопрос. Вы также можете добавить, что идиома макросов также предотвращает возможно более опасное (потому что нет ошибок) непреднамеренное поведение с помощью простых операторов if:
#define FOO(x) f(x); g(x)
if (test) FOO( baz);
расширяется до:
if (test) f(baz); g(baz);
который синтаксически верен, поэтому ошибки компилятора нет, но имеет, вероятно, непреднамеренное последствие, что g () всегда будет вызываться.
"вероятно, непреднамеренно"? Я бы сказал «конечно непреднамеренно», иначе программиста нужно вытащить и расстрелять (а не жестко наказать кнутом).
Или они могут получить повышение, если они работают в трехбуквенном агентстве и тайком вставляют этот код в широко используемую программу с открытым исходным кодом ... :-)
И этот комментарий просто напоминает мне строку goto fail в недавней ошибке проверки сертификата SSL, обнаруженной в ОС Apple.
Макросы - это скопированные / вставленные фрагменты текста, которые препроцессор вставит в подлинный код; автор макроса надеется, что замена даст правильный код.
Есть три хороших "совета", чтобы добиться в этом успеха:
Нормальный код обычно заканчивается точкой с запятой. Если пользователь просматривает код, который не нужен ...
doSomething(1) ;
DO_SOMETHING_ELSE(2) // <== Hey? What's this?
doSomethingElseAgain(3) ;
Это означает, что пользователь ожидает, что компилятор выдаст ошибку, если точка с запятой отсутствует.
Но настоящая серьезная причина заключается в том, что в какой-то момент автору макроса, возможно, потребуется заменить макрос настоящей функцией (возможно, встроенной). Таким образом, макрос должен вести себя как В самом деле.
Итак, у нас должен быть макрос, для которого нужна точка с запятой.
Как показано в ответе jfm3, иногда макрос содержит более одной инструкции. И если макрос используется внутри оператора if, это будет проблематично:
if (bIsOk)
MY_MACRO(42) ;
Этот макрос может быть расширен как:
#define MY_MACRO(x) f(x) ; g(x)
if (bIsOk)
f(42) ; g(42) ; // was MY_MACRO(42) ;
Функция g будет выполняться независимо от значения bIsOk.
Это означает, что мы должны добавить в макрос область видимости:
#define MY_MACRO(x) { f(x) ; g(x) ; }
if (bIsOk)
{ f(42) ; g(42) ; } ; // was MY_MACRO(42) ;
Если макрос выглядит примерно так:
#define MY_MACRO(x) int i = x + 1 ; f(i) ;
У нас может быть другая проблема в следующем коде:
void doSomething()
{
int i = 25 ;
MY_MACRO(32) ;
}
Потому что он расширится как:
void doSomething()
{
int i = 25 ;
int i = 32 + 1 ; f(i) ; ; // was MY_MACRO(32) ;
}
Конечно, этот код не компилируется. Итак, опять же, решение использует область видимости:
#define MY_MACRO(x) { int i = x + 1 ; f(i) ; }
void doSomething()
{
int i = 25 ;
{ int i = 32 + 1 ; f(i) ; } ; // was MY_MACRO(32) ;
}
Код снова работает правильно.
Есть одна идиома C / C++, которая производит такой эффект: цикл do / while:
do
{
// code
}
while(false) ;
Команда do / while может создавать область видимости, таким образом инкапсулируя код макроса, и в конце требует точки с запятой, таким образом расширяясь до кода, в котором она нужна.
Бонус?
Компилятор C++ оптимизирует цикл do / while, так как тот факт, что его пост-условие ложно, известен во время компиляции. Это означает, что такой макрос, как:
#define MY_MACRO(x) \
do \
{ \
const int i = x + 1 ; \
f(i) ; g(i) ; \
} \
while(false)
void doSomething(bool bIsOk)
{
int i = 25 ;
if (bIsOk)
MY_MACRO(42) ;
// Etc.
}
будет правильно расширяться как
void doSomething(bool bIsOk)
{
int i = 25 ;
if (bIsOk)
do
{
const int i = 42 + 1 ; // was MY_MACRO(42) ;
f(i) ; g(i) ;
}
while(false) ;
// Etc.
}
а затем компилируется и оптимизируется как
void doSomething(bool bIsOk)
{
int i = 25 ;
if (bIsOk)
{
f(43) ; g(43) ;
}
// Etc.
}
Обратите внимание, что изменение макроса на встроенную функцию изменяет некоторые стандартные предопределенные макросы, например следующий код показывает изменение в НАЗНАЧЕНИЕ и ЛИНИЯ: #include <stdio.h> #define Fmacro () printf ("% s% d \ n", НАЗНАЧЕНИЕ, ЛИНИЯ) inline void Finline () {printf ("% s% d \ n ", НАЗНАЧЕНИЕ, ЛИНИЯ); } int main () {Fmacro (); Finline (); возврат 0; } (жирные термины должны быть заключены в двойные подчеркивания - плохое форматирование!)
Есть ряд мелких, но не совсем несущественных проблем с этим ответом. Например: void doSomething() { int i = 25 ; { int i = x + 1 ; f(i) ; } ; // was MY_MACRO(32) ; } - неправильное расширение; x в расширении должно быть 32. Более сложный вопрос - что такое расширение MY_MACRO(i+7). И еще одно расширение MY_MACRO(0x07 << 6). Есть много хорошего, но есть некоторые незакрещенные и незакрашенные «i».
@Gnubie: я на случай, если вы все еще здесь, и вы еще не поняли этого: вы можете убрать звездочки и подчеркивания в комментариях с помощью обратной косой черты, поэтому, если вы наберете \_\_LINE\_\_, он отобразится как __LINE__. ИМХО, лучше просто использовать форматирование кода для кода; например, __LINE__ (не требует особого обращения). P.S. Не знаю, было ли это правдой в 2012 году; С тех пор они внесли в двигатель немало улучшений.
Признавая, что мой комментарий опоздал на шесть лет, но большинство компиляторов C на самом деле не встраивают функции inline (как разрешено стандартом)
Я не думаю, что это было упомянуто, так что подумайте об этом
while(i<100)
FOO(i++);
будет переведен на
while(i<100)
do { f(i++); g(i++); } while (0)
обратите внимание, как макрос i++ оценивается дважды. Это может привести к интересным ошибкам.
Это не имеет ничего общего с конструкцией do ... while (0).
Правда. Но имеет отношение к теме макросов и функций, а также к тому, как написать макрос, который ведет себя как функция ...
Как и в предыдущем случае, это не ответ, а комментарий. По теме: вот почему вы используете материал только один раз: do { int macroname_i = (i); f(macroname_i); g(macroname_i); } while (/* CONSTCOND */ 0)
Хотя ожидается, что компиляторы оптимизируют циклы do { ... } while(false);, существует другое решение, которое не требует такой конструкции. Решение - использовать оператор запятой:
#define FOO(X) (f(X),g(X))
или даже более экзотично:
#define FOO(X) g((f(X),(X)))
Хотя это будет хорошо работать с отдельными инструкциями, это не будет работать со случаями, когда переменные создаются и используются как часть #define:
#define FOO(X) (int s=5,f((X)+s),g((X)+s))
С этим придется использовать конструкцию do / while.
спасибо, поскольку оператор запятой не гарантирует порядок выполнения, это вложение - способ обеспечить это.
@Marius: Ложь; оператор запятой является точкой последовательности и, таким образом, делает гарантирует порядок выполнения. Я подозреваю, что вы перепутали его с запятой в списках аргументов функций.
Второе экзотическое предложение сделало мой день лучше.
Просто хотел добавить, что компиляторы вынуждены сохранять наблюдаемое поведение программы, поэтому оптимизация do / while away не имеет большого значения (при условии, что оптимизация компилятора верна).
@MarcoA. хотя вы правы, в прошлом я обнаружил, что оптимизация компилятора, точно сохраняя функцию кода, но изменяя строки, которые, казалось бы, ничего не делали в единственном контексте, нарушает многопоточные алгоритмы. Речь идет о пункте Peterson's Algorithm.
Это также не будет работать для всех видов конструкций, хотя C с тернарным оператором и this довольно выразителен.
Почему-то не могу прокомментировать первый ответ ...
Некоторые из вас показали макросы с локальными переменными, но никто не упомянул, что вы не можете просто использовать любое имя в макросе! Когда-нибудь он укусит пользователя! Почему? Потому что входные аргументы подставляются в ваш шаблон макроса. И в примерах макросов вы используете, вероятно, наиболее часто используемое имя переменной я.
Например, когда следующий макрос
#define FOO(X) do { int i; for (i = 0; i < (X); ++i) do_something(i); } while (0)
используется в следующей функции
void some_func(void) {
int i;
for (i = 0; i < 10; ++i)
FOO(i);
}
макрос будет использовать не предполагаемую переменную i, которая объявлена в начале some_func, а локальную переменную, объявленную в цикле do ... while макроса.
Таким образом, никогда не используйте в макросе общие имена переменных!
Обычно в макросах добавляются символы подчеркивания в имена переменных, например int __i;.
@Blaisorblade: На самом деле это неверно и незаконно C; ведущие подчеркивания зарезервированы для использования реализацией. Причина, по которой вы видели этот «обычный шаблон», - это чтение системных заголовков («реализация»), которые должны ограничиваться этим зарезервированным пространством имен. Для приложений / библиотек вы должны выбрать свои собственные непонятные, маловероятные имена без подчеркивания, например mylib_internal___i или аналогичный.
@R .. Вы правы - я на самом деле читал это в "приложении", ядре Linux, но в любом случае это исключение, поскольку оно не использует стандартную библиотеку (технически, вместо этого, "автономная" реализация C "размещенного").
@R .. это не совсем правильно: начальные подчеркивания с последующим заглавным или вторым подчеркиванием зарезервированы для реализации во всех контекстах. Начальные подчеркивания, за которыми следует что-то еще, не зарезервированы в локальной области.
@Leushenko: Да, но различие достаточно тонкое, и я считаю, что лучше всего сказать людям, чтобы они вообще не использовали такие имена. Люди, которые понимают эту тонкость, вероятно, уже знают, что я замалчиваю детали. :-)
Хотя это правильно, это не ответ на вопрос. По правилам сайта, пожалуйста, сделайте это в комментарии; если вы не можете, сначала соберите карму из хороших ответов (которые на самом деле являются ответами); да, первые шаги могут быть трудными, но в SO все это возможно.
Приведенные выше ответы объясняют значение этих конструкций, но между ними есть существенная разница, о которой не упоминалось. Фактически, есть причина предпочесть do ... while конструкции if ... else.
Проблема конструкции if ... else в том, что она не позволяет сила ставить точку с запятой. Как в этом коде:
FOO(1)
printf("abc");
Хотя мы не указали точку с запятой (по ошибке), код расширится до
if (1) { f(X); g(X); } else
printf("abc");
и будет компилироваться незаметно (хотя некоторые компиляторы могут выдавать предупреждение о недоступности кода). Но оператор printf никогда не будет выполнен.
Конструкция do ... while не имеет такой проблемы, поскольку единственный действительный токен после while(0) - это точка с запятой.
вот почему вы должны сделать #define FOO(X) if (1) { f(X); g(X); } else (void)0
@RichardHansen: Все еще не так хорошо, потому что, глядя на вызов макроса, вы не знаете, расширяется ли он до оператора или до выражения. Если кто-то предположит позднее, он может написать FOO(1),x++;, что снова даст нам ложное срабатывание. Просто используйте do ... while и все.
Чтобы избежать недоразумений, достаточно документировать макрос. Я согласен с тем, что do ... while (0) предпочтительнее, но у него есть один недостаток: break или continue будут управлять циклом do ... while (0), а не циклом, содержащим вызов макроса. Так что трюк с if все еще имеет значение.
Я не понимаю, где вы могли бы разместить break или continue, которые будут видны внутри вашего псевдопетля макроса do {...} while(0). Даже в параметре макроса будет синтаксическая ошибка.
Другой причиной использования конструкции do { ... } while(0) вместо конструкции if whatever является ее идиоматическая природа. Конструкция do {...} while(0) широко распространена, хорошо известна и часто используется многими программистами. Его обоснование и документация известны. Не так для конструкции if. Следовательно, при проверке кода требуется меньше усилий для проверки.
@tristopia: я видел, как люди пишут макросы, которые принимают блоки кода в качестве аргументов (что я не обязательно рекомендую). Например: #define CHECK(call, onerr) if (0 != (call)) { onerr } else (void)0. Его можно использовать как CHECK(system("foo"), break;);, где break; предназначен для обозначения цикла, включающего вызов CHECK().
do {} while (0) и if (1) {} else должны убедиться, что макрос расширен только до 1 инструкции. Иначе:
if (something)
FOO(X);
расширится до:
if (something)
f(X); g(X);
И g(X) будет выполняться вне управляющего оператора if. Этого можно избежать при использовании do {} while (0) и if (1) {} else.
С GNU выражение выражения (не частью стандарта C) у вас есть лучший способ, чем do {} while (0) и if (1) {} else, решить эту проблему, просто используя ({}):
#define FOO(X) ({f(X); g(X);})
И этот синтаксис совместим с возвращаемыми значениями (обратите внимание, что do {} while (0) нет), например:
return FOO("X");
использование блокировки блока {} в макросе было бы достаточным для объединения кода макроса, чтобы все выполнялись для одного и того же пути if-condition. do-while around используется для установки точки с запятой в местах, где используется макрос. таким образом, макрос принудительно ведет себя подобным образом. это включает требование наличия конечной точки с запятой при использовании.
Библиотека препроцессора P99 Йенса Густедта (да, тот факт, что такая вещь существует, тоже поразил меня!) Улучшает конструкцию if (1) { ... } else небольшим, но значительным образом, определяя следующее:
#define P99_NOP ((void)0)
#define P99_PREFER(...) if (1) { __VA_ARGS__ } else
#define P99_BLOCK(...) P99_PREFER(__VA_ARGS__) P99_NOP
Обоснование этого состоит в том, что, в отличие от конструкции do { ... } while(0), break и continue все еще работают внутри данного блока, но ((void)0) создает синтаксическую ошибку, если точка с запятой опущена после вызова макроса, что в противном случае пропустило бы следующий блок. (На самом деле здесь нет проблемы с «болтающимся остальным», поскольку else связывается с ближайшим if, который указан в макросе.)
Если вас интересуют вещи, которые можно сделать более или менее безопасно с помощью препроцессора C, посмотрите эту библиотеку.
Хотя это очень умно, это приводит к тому, что компилятор забрасывает его предупреждениями о потенциальном подвешивании else.
Обычно вы используете макросы для создания замкнутой среды, то есть вы никогда не используете break (или continue) внутри макроса для управления циклом, который начался / закончился снаружи, это просто плохой стиль и скрывает потенциальные точки выхода.
В Boost также есть библиотека препроцессора. Что в этом умопомрачительного?
Риск с else ((void)0) заключается в том, что кто-то может писать YOUR_MACRO(), f();, и он будет синтаксически корректным, но никогда не вызовет f(). С dowhile это синтаксическая ошибка.
@melpomene так что насчет else do; while (0)?
Для примера с else я бы добавил выражение типа
voidв конце ... например, ((недействительно) 0).