Во многих макросах 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
и 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
, это приводит к " dangling else", что могло бы еще более затруднить обнаружение существующей проблемы с болтанием еще, как в следующем коде.
if (corge)
if (1) { f(corge); g(corge); } else;
else
gralt();
Цель состоит в том, чтобы использовать точку с запятой в контекстах, где ошибочная точка с запятой ошибочна. Конечно, в этот момент можно (и, вероятно, следует) утверждать, что было бы лучше объявить BAR
как действительную функцию, а не макрос.
Таким образом, do ... while
используется для устранения недостатков препроцессора C. Когда эти руководства по стилю C подскажут вам убрать препроцессор C, это то, о чем они беспокоятся.
Макросы - это скопированные/вставляемые фрагменты текста, которые препроцессор будет помещать в подлинный код; автор макроса надеется, что замена произведет правильный код.
Есть три хороших "подсказки", которые могут преуспеть в этом:
Обычный код обычно заканчивается точкой с запятой. Если пользователь просматривает код, не нуждающийся в одном...
doSomething(1) ;
DO_SOMETHING_ELSE(2) // <== Hey? What 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/С++, которая производит этот эффект: цикл do/while:
do
{
// code
}
while(false) ;
Функция do/while может создавать область видимости, таким образом инкапсулируя макрокод, и в конце нуждается в полутоле в конце, тем самым расширяя ее в код, требующий этого.
Бонус?
Компилятор С++ оптимизирует цикл 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.
}
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)
. Есть много хороших вещей, но есть несколько не пунктирных я и непересекающихся.
@jfm3 - У вас есть хороший ответ на вопрос. Вы также можете добавить, что идиома макроса также предотвращает возможное более опасное (потому что нет ошибки) непреднамеренное поведение с простыми операциями "if":
#define FOO(x) f(x); g(x)
if (test) FOO( baz);
расширяется до:
if (test) f(baz); g(baz);
который синтаксически корректен, поэтому нет ошибки компилятора, но имеет, вероятно, непреднамеренное последствие того, что g() всегда будет вызываться.
В приведенных выше ответах объясняется смысл этих конструкций, но между ними не существует существенной разницы. На самом деле, есть причина предпочесть 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
FOO(1),x++;
что снова даст нам ложный положительный результат. Просто используйте do ... while
и все.
Хотя ожидается, что компиляторы оптимизируют петли 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.
Jens Gustedt Препроцессорная библиотека 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, проверьте эту библиотеку.
break
(или continue
) внутри макроса для управления циклом, который начинается / заканчивается снаружи, это просто плохой стиль и скрывает потенциальные точки выхода.
По некоторым причинам я не могу прокомментировать первый ответ...
Некоторые из вас показывали макросы с локальными переменными, но никто не упоминал, что вы не можете просто использовать любое имя в макросе! Он укусит пользователя в один прекрасный день! Зачем? Поскольку входные аргументы подставляются в ваш шаблон макроса. И в ваших примерах макросов вы используете, вероятно, наиболее часто используемое измененное имя i.
Например, если следующий макрос
#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;
,
mylib_internal___i
или аналогичные.
Я не думаю, что это было упомянуто, поэтому рассмотрим этот
while(i<100)
FOO(i++);
будет переведен в
while(i<100)
do { f(i++); g(i++); } while (0)
Обратите внимание, что i++
дважды вычисляется макросом. Это может привести к некоторым интересным ошибкам.
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");
Я нашел, что этот трюк очень полезен в ситуациях, когда вам нужно последовательно обрабатывать определенное значение. На каждом уровне обработки, если возникает некоторая ошибка или недействительное условие, вы можете избежать дальнейшей обработки и вырваться раньше. например.
#define CALL_AND_RETURN(x) if ( x() == false) break;
do {
CALL_AND_RETURN(process_first);
CALL_AND_RETURN(process_second);
CALL_AND_RETURN(process_third);
//(simply add other calls here)
} while (0);
if (process_first() && process_second() && process_third()) { // do whatever, all success }
и иметь свободу использования аргументов функции? Это также дает гибкость для других возвращаемых значений в случае успеха (например, >= 0
) без необходимости создавать дополнительный макрос.
Причина, по которой do {} while (0)
используется более, if (1) {}
состоит в том, что никто не может изменить код перед вызовом макроса do {} while (0)
это какой-то другой тип блока. Например, если вы вызвали макрос, окруженный if (1) {}
вроде:
else
MACRO(x);
Это на самом деле else if
. Тонкая разница
void
... like ((void) 0) .do while
не совместима с операторами return, поэтому конструкцияif (1) { ... } else ((void)0)
имеет более совместимые применения в стандарте C. А в GNU C вы предпочтете конструкция описана в моем ответе.