Зачем использовать явно бессмысленные операторы do-while и if-else в макросах?
Во многих макросах 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)
11 ответов
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
расширяет приведенный выше код следующим образом, что является синтаксически правильным.
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
это приводит к " висячему другому", что может усложнить поиск существующей висячей другой ситуации, как в следующем коде.
if (corge)
if (1) { f(corge); g(corge); } else;
else
gralt();
Смысл в том, чтобы использовать точку с запятой в тех случаях, когда висячая точка с запятой ошибочна. Конечно, на этом этапе можно (и, вероятно, следует) утверждать, что было бы лучше объявить BAR
как фактическая функция, а не макрос.
Таким образом, do ... while
есть, чтобы обойти недостатки препроцессора C. Когда эти руководства по стилю С говорят вам уволить препроцессор С, это то, о чем они беспокоятся.
Макросы - это скопированные / вставленные фрагменты текста, которые препроцессор вставит в подлинный код; Автор макроса надеется, что замена даст действительный код.
В этом есть три хороших "совета":
Помогите макросу вести себя как настоящий код
Обычный код обычно заканчивается точкой с запятой. Если пользователь просматривает код, который ему не нужен...
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) ;
Введите действительный код 2
Если макрос что-то вроде:
#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.
}
@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)
это точка с запятой.
объяснение
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");
Хотя ожидается, что компиляторы оптимизируют 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, посмотрите эту библиотеку.
По некоторым причинам я не могу прокомментировать первый ответ...
Некоторые из вас показали макросы с локальными переменными, но никто не упомянул, что вы не можете просто использовать любое имя в макросе! Это когда-нибудь укусит пользователя! Зачем? Потому что входные аргументы подставляются в ваш шаблон макроса. И в ваших примерах макросов вы используете, вероятно, наиболее часто используемое имя переменной 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 макроса.
Таким образом, никогда не используйте общие имена переменных в макросе!
Я не думаю, что это было упомянуто, так что подумайте об этом
while(i<100)
FOO(i++);
будет переведен на
while(i<100)
do { f(i++); g(i++); } while (0)
обратите внимание, как i++
оценивается дважды макросом. Это может привести к некоторым интересным ошибкам.
Я нашел этот трюк очень полезным в ситуациях, когда вам нужно последовательно обрабатывать определенное значение. На каждом уровне обработки, если возникает какая-либо ошибка или недопустимое условие, вы можете избежать дальнейшей обработки и выйти из строя рано. например
#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);
Причина do {} while (0)
используется более if (1) {}
в том, что нет способа изменить код перед вызовом макроса do {} while (0)
быть каким-то другим видом блока. Например, если вы вызвали макрос, окруженный if (1) {}
лайк:
else
MACRO(x);
Это на самом деле else if
, Тонкая разница