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

StackOverflow https://stackoverflow.com/questions/154136

  •  03-07-2019
  •  | 
  •  

Вопрос

Во многих макросах 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 и 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'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, поскольку тот факт, что его последующее условие равно false, известен во время компиляции.Это означает, что макрос, подобный:

#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(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.

У Йенса Густедта Библиотека препроцессора 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", поскольку 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 макроса.

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

Объяснение

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

Я не думаю, что об этом упоминалось, так что подумайте вот о чем

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);
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top