Зачем использовать явно бессмысленные операторы do-while и if-else в макросах?

718

Во многих макросах 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)
  • 2
    Для примера с else, я бы добавил в конце выражение типа void ... like ((void) 0) .
  • 0
    Напомним, что конструкция do while не совместима с операторами return, поэтому конструкция if (1) { ... } else ((void)0) имеет более совместимые применения в стандарте C. А в GNU C вы предпочтете конструкция описана в моем ответе.
Теги:
c-preprocessor
c++-faq

11 ответов

774
Лучший ответ

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, это то, о чем они беспокоятся.

  • 0
    В моем собственном ответе пропущена «проблема еще, если скобки и точка с запятой», но вы забыли проблему с областью видимости, а также оптимизацию компилятора do / while (false) ... :-p ...
  • 19
    Вероятно, лучший пример того, почему вы НЕ ДОЛЖНЫ делать это ...
Показать ещё 13 комментариев
145

Макросы - это скопированные/вставляемые фрагменты текста, которые препроцессор будет помещать в подлинный код; автор макроса надеется, что замена произведет правильный код.

Есть три хороших "подсказки", которые могут преуспеть в этом:

Помогите макросу вести себя как подлинный код

Обычный код обычно заканчивается точкой с запятой. Если пользователь просматривает код, не нуждающийся в одном...

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) ;

Произвести действительный код 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/С++, которая производит этот эффект: цикл 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.
}
  • 6
    Обратите внимание, что изменение макросов на встроенную функцию изменяет некоторые стандартные предопределенные макросы, например, следующий код показывает изменение в FUNCTION и LINE : #include <stdio.h> #define Fmacro () printf ("% s% d \ n", FUNCTION , LINE ) inline void Finline () {printf ("% s% d \ n", FUNCTION , LINE ); } int main () {Fmacro (); Finline (); вернуть 0; } (жирные термины должны быть заключены в двойное подчеркивание - плохой форматер!)
  • 5
    В этом ответе есть ряд незначительных, но не совсем несущественных вопросов. Например: 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) . Есть много хороших вещей, но есть несколько не пунктирных я и непересекающихся.
Показать ещё 2 комментария
48

@jfm3 - У вас есть хороший ответ на вопрос. Вы также можете добавить, что идиома макроса также предотвращает возможное более опасное (потому что нет ошибки) непреднамеренное поведение с простыми операциями "if":

#define FOO(x)  f(x); g(x)

if (test) FOO( baz);

расширяется до:

if (test) f(baz); g(baz);

который синтаксически корректен, поэтому нет ошибки компилятора, но имеет, вероятно, непреднамеренное последствие того, что g() всегда будет вызываться.

  • 20
    "вероятно непреднамеренно"? Я бы сказал «конечно, непреднамеренно», иначе программист должен быть убит и застрелен (в отличие от того, чтобы быть полностью наказанным кнутом).
  • 4
    Или им могут дать повышение, если они работают в трехбуквенном агентстве и тайно вставляют этот код в широко используемую программу с открытым исходным кодом ... :-)
Показать ещё 1 комментарий
23

В приведенных выше ответах объясняется смысл этих конструкций, но между ними не существует существенной разницы. На самом деле, есть причина предпочесть do ... while конструкции if ... else.

Проблема конструкции if ... else заключается в том, что она не заставляет вас помещать точку с запятой. Как в этом коде:

FOO(1)
printf("abc");

Хотя мы оставили точку с запятой (по ошибке), код будет расширяться до

if (1) { f(X); g(X); } else
printf("abc");

и будет автоматически компилироваться (хотя некоторые компиляторы могут выдать предупреждение для недостижимого кода). Но оператор printf никогда не будет выполнен.

Конструкция

do ... while не имеет такой проблемы, так как единственный действительный токен после while(0) является точкой с запятой.

  • 0
    вот почему вы должны сделать #define FOO(X) if (1) { f(X); g(X); } else (void)0
  • 2
    @RichardHansen: Все еще не так хорошо, потому что, глядя на вызов макроса, вы не знаете, распространяется ли он на оператор или выражение. Если кто-то предполагает позднее, она может написать FOO(1),x++; что снова даст нам ложный положительный результат. Просто используйте do ... while и все.
Показать ещё 4 комментария
15

Хотя ожидается, что компиляторы оптимизируют петли 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.

  • 3
    g ((f (X), (X))) это очень милый трюк!
  • 0
    спасибо, поскольку оператор запятой не гарантирует порядок выполнения, это вложение - способ обеспечить это.
Показать ещё 5 комментариев
9

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, проверьте эту библиотеку.

  • 0
    Хотя это очень умно, это приводит к тому, что один человек подвергается бомбардировке предупреждениями компилятора о возможном зависании еще.
  • 0
    Обычно вы используете макросы для создания изолированной среды, то есть вы никогда не используете break (или continue ) внутри макроса для управления циклом, который начинается / заканчивается снаружи, это просто плохой стиль и скрывает потенциальные точки выхода.
Показать ещё 1 комментарий
6

По некоторым причинам я не могу прокомментировать первый ответ...

Некоторые из вас показывали макросы с локальными переменными, но никто не упоминал, что вы не можете просто использовать любое имя в макросе! Он укусит пользователя в один прекрасный день! Зачем? Поскольку входные аргументы подставляются в ваш шаблон макроса. И в ваших примерах макросов вы используете, вероятно, наиболее часто используемое измененное имя 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.

Таким образом, никогда не используйте имена общих переменных в макросе!

  • 0
    Обычный шаблон заключается в добавлении подчеркивания в имена переменных в макросах - например, int __i; ,
  • 8
    @Blaisorblade: На самом деле это неправильно и незаконно C; ведущие подчеркивания зарезервированы для использования реализацией. Причина, по которой вы видели этот «обычный шаблон», связана с чтением системных заголовков («реализация»), которые должны ограничивать себя этим зарезервированным пространством имен. Для приложений / библиотек, вы должны выбрать свои собственные непонятные, маловероятные имена без подчеркивания, например, mylib_internal___i или аналогичные.
Показать ещё 4 комментария
3

Я не думаю, что это было упомянуто, поэтому рассмотрим этот

while(i<100)
  FOO(i++);

будет переведен в

while(i<100)
  do { f(i++); g(i++); } while (0)

Обратите внимание, что i++ дважды вычисляется макросом. Это может привести к некоторым интересным ошибкам.

  • 14
    Это не имеет ничего общего с конструкцией do ... while (0).
  • 2
    Правда. Но лучше поговорить о теме макросов и функций и о том, как написать макрос, который ведет себя как функция ...
Показать ещё 1 комментарий
2

объяснение

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");
  • 1
    использование блокировки блока {} в макросе было бы достаточно для связывания кода макроса, чтобы все выполнялось по одному и тому же пути if-условия. do-while around используется для принудительного использования точки с запятой в местах, где используется макрос. таким образом, макрос принудительно ведет себя как функция. это включает в себя требование запятой через запятую при использовании.
1

Я нашел, что этот трюк очень полезен в ситуациях, когда вам нужно последовательно обрабатывать определенное значение. На каждом уровне обработки, если возникает некоторая ошибка или недействительное условие, вы можете избежать дальнейшей обработки и вырваться раньше. например.

#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);
  • 0
    Я видел это тоже так.
  • 1
    Я предпочел бы иметь if (process_first() && process_second() && process_third()) { // do whatever, all success } и иметь свободу использования аргументов функции? Это также дает гибкость для других возвращаемых значений в случае успеха (например, >= 0 ) без необходимости создавать дополнительный макрос.
Показать ещё 2 комментария
0

Причина, по которой do {} while (0) используется более, if (1) {} состоит в том, что никто не может изменить код перед вызовом макроса do {} while (0) это какой-то другой тип блока. Например, если вы вызвали макрос, окруженный if (1) {} вроде:

else
  MACRO(x);

Это на самом деле else if. Тонкая разница

  • 0
    Десятилетние ответы на этой странице уже говорят это.
Сообщество Overcoder
Наверх
Меню