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

c++ c c-preprocessor c++-faq

87112 просмотра

10 ответа

Во многих макросах 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)
Автор: jfm3 Источник Размещён: 22.10.2019 02:54

Ответы (10)


787 плюса

Решение

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

Автор: jfm3 Размещён: 30.09.2008 05:36

147 плюса

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

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

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

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

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.
}
Автор: paercebal Размещён: 30.09.2008 06:12

51 плюса

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

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

if (test) FOO( baz);

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

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

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

Автор: Michael Burr Размещён: 30.09.2008 06:05

23 плюса

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

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

FOO(1)
printf("abc");

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

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

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

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

Автор: ybungalobill Размещён: 03.08.2012 03:21

16 плюса

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

Автор: Marius Размещён: 10.10.2009 08:27

10 плюса

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

Автор: Isaac Schwabacher Размещён: 19.11.2014 04:19

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 макроса.

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

Автор: Mike Meyer Размещён: 21.12.2011 06:42

3 плюса

объяснение

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");
Автор: Cœur Размещён: 23.03.2014 12:04

2 плюса

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

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

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

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

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

Автор: John Nilsson Размещён: 18.10.2008 10:04

-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);
Автор: pankaj Размещён: 24.06.2014 05:45
Вопросы из категории :
32x32