it-swarm.com.ru

Зачем использовать явно бессмысленные операторы 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)
693
jfm3

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. Когда эти руководства по стилю С говорят вам уволить препроцессор С, это то, о чем они беспокоятся.

752
jfm3

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

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

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

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

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

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

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

if (test) FOO( baz);

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

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

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

50
Michael Burr

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

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

FOO(1)
printf("abc");

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

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

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

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

23
ybungalobill

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

15
Marius

Библиотека препроцессора 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, посмотрите эту библиотеку.

10
Isaac Schwabacher

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

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

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

6
Mike Meyer

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

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

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

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

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

3
John Nilsson

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

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

Объяснение

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
Cœur

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

else
  MACRO(x);

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

0
ericcurtin