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

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

771 голосов
/ 30 сентября 2008

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, это приводит к " висящему"", что может усложнить поиск существующей висячей проблемы, как в следующем коде.

if (corge)
  if (1) { f(corge); g(corge); } else;
else
  gralt();

Смысл в том, чтобы использовать точку с запятой в тех случаях, когда висящая точка с запятой ошибочна. Конечно, на этом этапе можно (и, вероятно, следует) утверждать, что было бы лучше объявить BAR как действительную функцию, а не как макрос.

В итоге, do ... while предназначен для обхода недостатков препроцессора C. Когда эти руководства по стилю С говорят вам уволить препроцессор С, это то, о чем они беспокоятся.

143 голосов
/ 30 сентября 2008

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

В этом есть три хороших «совета»:

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

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

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.
}
51 голосов
/ 30 сентября 2008

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

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

if (test) FOO( baz);

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

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

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

23 голосов
/ 03 августа 2012

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

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

FOO(1)
printf("abc");

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

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

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

Конструкция

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

16 голосов
/ 10 октября 2009

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

10 голосов
/ 19 ноября 2014

Библиотека препроцессора P99 * Jens Gustedt (да, тот факт, что такая вещь существует, поразила и меня!) Улучшает конструкцию 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, посмотрите эту библиотеку.

6 голосов
/ 21 декабря 2011

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

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

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

3 голосов
/ 23 марта 2014

Объяснение

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");
2 голосов
/ 19 октября 2008

Не думаю, что это было упомянуто

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

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

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

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

1 голос
/ 24 июня 2014

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

#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);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...