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

Во многих макросах 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)

Ответ 1

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

Ответ 2

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

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

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

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

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.
}

Ответ 3

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

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

if (test) FOO( baz);

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

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

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

Ответ 4

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

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

FOO(1)
printf("abc");

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

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

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

Конструкция

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

Ответ 5

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

Ответ 6

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

Ответ 7

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

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

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

Ответ 8

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

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

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

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

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

Ответ 9

объяснение

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

Ответ 10

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

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

Ответ 11

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

else
  MACRO(x);

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