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


788

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

2
Для примера с else я бы добавил выражение voidтипа в конце ... like ((void) 0) .
Phil1970

1
Напомним, что do whileконструкция несовместима с операторами return, поэтому if (1) { ... } else ((void)0)конструкция имеет более совместимые применения в стандарте C. А в GNU C вы предпочтете конструкцию, описанную в моем ответе.
Cœur

Ответы:


829

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


18
Разве это не сильный аргумент, чтобы всегда использовать фигурные скобки в операторах if, while и for? Если вы всегда делаете это (как, например, требуется для MISRA-C), описанная выше проблема исчезнет.
Стив Мельникофф,

17
Пример запятой должен быть в #define BAR(X) (f(X), g(X))противном случае приоритет оператора может испортить семантику.
Стюарт

20
@DawidFerenczy: хотя и вы, и я от четырех с половиной лет назад сделали хорошую мысль, мы должны жить в реальном мире. Если мы не можем гарантировать, что все ifоператоры и т. Д. В нашем коде используют фигурные скобки, то перенос таких макросов является простым способом избежать проблем.
Стив Мельникофф,

8
Примечание. if(1) {...} else void(0)Форма безопаснее do {...} while(0)макросов for, параметрами которых является код, включенный в расширение макроса, поскольку он не изменяет поведение ключевых слов break или continue. Например: for (int i = 0; i < max; ++i) { MYMACRO( SomeFunc(i)==true, {break;} ) }вызывает неожиданное поведение, когда MYMACROоно определено как, #define MYMACRO(X, CODE) do { if (X) { cout << #X << endl; {CODE}; } } while (0)потому что разрыв влияет на цикл while макроса, а не на цикл for на сайте вызова макроса.
Крис Клайн

5
@ace void(0)был опечаткой, я имел в виду (void)0. И я считаю , что это действительно решить «оборванных» еще проблема: извещение нет никакого точка с запятой после (void)0. В этом случае висящий еще (например if (cond) if (1) foo() else (void)0 else { /* dangling else body */ }) вызывает ошибку компиляции. Вот живой пример, демонстрирующий это
Крис Клайн

153

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

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

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

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

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

6
Обратите внимание, что изменение макросов на встроенную функцию изменяет некоторые стандартные предопределенные макросы, например, следующий код показывает изменение в FUNCTION и LINE : #include <stdio.h> #define Fmacro () printf ("% s% d \ n", FUNCTION , LINE ) inline void Finline () {printf ("% s% d \ n", FUNCTION , LINE ); } int main () {Fmacro (); Finline (); вернуть 0; } (жирные термины должны быть заключены в двойные подчеркивания - плохой форматер!)
Gnubie

6
В этом ответе есть ряд незначительных, но не совсем несущественных вопросов. Например: void doSomething() { int i = 25 ; { int i = x + 1 ; f(i) ; } ; // was MY_MACRO(32) ; }неправильное расширение; xв расширении должно быть 32. Более сложным вопросом является то , что это расширение MY_MACRO(i+7). И еще это расширение MY_MACRO(0x07 << 6). Есть много хороших вещей, но есть несколько не пунктирных я и непересекающихся.
Джонатан Леффлер

@Gnubie: Я полагаю, что вы все еще здесь, и вы до сих пор не поняли этого: вы можете избежать звездочек и подчеркиваний в комментариях с обратной косой чертой, так что если вы \_\_LINE\_\_наберете, он будет отображаться как __LINE__ ИМХО, лучше просто использовать форматирование кода для кода; например, __LINE__(который не требует специальной обработки). PS Я не знаю, было ли это правдой в 2012 году; с тех пор они внесли несколько улучшений в двигатель.
Скотт

1
Признавая, что мой комментарий опоздал на шесть лет, но большинство компиляторов Си на самом деле не выполняют встроенные inlineфункции (как это разрешено стандартом)
Эндрю

53

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

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

if (test) FOO( baz);

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

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

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


22
"вероятно непреднамеренно"? Я бы сказал «конечно, непреднамеренно», иначе программист должен быть убит и застрелен (в отличие от того, чтобы быть полностью наказанным кнутом).
Лоуренс Дол

4
Или им могут дать повышение, если они работают в трехбуквенном агентстве и тайно вставляют этот код в широко используемую программу с открытым исходным кодом ... :-)
R .. GitHub ОСТАНОВИТЬ ПОМОЩЬ ICE

2
И этот комментарий просто напоминает мне о строке goto fail в недавней ошибке проверки SSL-сертификата, обнаруженной в операционных системах Apple
Джерард Секстон,

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)с запятой.


2
@RichardHansen: Все еще не так хорошо, потому что, глядя на вызов макроса, вы не знаете, распространяется ли он на оператор или на выражение. Если кто-то предполагает позднее, она может написать, FOO(1),x++;что снова даст нам ложный позитив. Просто используйте do ... whileи все.
Яков Галка

1
Документирования макроса, чтобы избежать недоразумений должно быть достаточно. Я согласен, что do ... while (0)это предпочтительнее, но у него есть один недостаток: A breakили continueбудет контролировать do ... while (0)цикл, а не цикл, содержащий вызов макроса. Таким образом, ifуловка все еще имеет значение.
Ричард Хансен

2
Я не вижу, где вы могли бы поместить a breakили a, continueкоторые были бы видны как внутри вашего макроса do {...} while(0)псевдо-цикла. Даже в параметре макроса это может привести к синтаксической ошибке.
Патрик Шлютер

5
Еще одна причина, чтобы использовать do { ... } while(0)вместо if whateverконструкции, это идиоматическая природа этого. do {...} while(0)Конструкция широко, хорошо известна и используется много многими программистами. Его обоснование и документация хорошо известны. Не так для ifконструкции. Поэтому требуется меньше усилий, чтобы выполнить анализ кода.
Патрик Шлютер

1
@tristopia: я видел, как люди пишут макросы, которые принимают блоки кода в качестве аргументов (что я не обязательно рекомендую). Например: #define CHECK(call, onerr) if (0 != (call)) { onerr } else (void)0. Это может быть использовано, например CHECK(system("foo"), break;);, где break;предназначен для ссылки на цикл, включающий CHECK()вызов.
Ричард Хансен

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.


спасибо, так как оператор запятой не гарантирует порядок выполнения, это вложение - способ обеспечить это.
Мариус

15
@Marius: Ложь; оператор запятой является точкой последовательности и , таким образом , делает гарантию порядок выполнения. Я подозреваю, что вы перепутали это с запятой в списках аргументов функции.
R .. GitHub ОСТАНОВИТЬ ЛЬДА

2
Второе экзотическое предложение сделало мой день.
Spidey

Хотелось бы добавить, что компиляторы вынуждены сохранять наблюдаемое поведение программы, поэтому оптимизация «сделай / уйди» не составляет большого труда (при условии правильной оптимизации компилятора).
Марко А.

@MarcoA. в то время как вы правы, в прошлом я обнаружил, что оптимизация компилятора, точно сохраняя функцию кода, но изменяя строки, которые, казалось бы, ничего не делают в единственном контексте, нарушает многопоточные алгоритмы. Показательный пример Peterson's Algorithm.
Мариус

11

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


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

1
Обычно вы используете макросы для создания изолированной среды, то есть вы никогда не используете break(или continue) внутри макроса для управления циклом, который начинается / заканчивается снаружи, это просто плохой стиль и скрывает потенциальные точки выхода.
Мирабилось

В Boost также есть библиотека препроцессоров. Что поразительно в этом?
Рене

Риск else ((void)0)состоит в том, что кто-то может писать, YOUR_MACRO(), f();и это будет синтаксически допустимым, но никогда не будет звонить f(). С do whileсинтаксической ошибкой.
Мельпомена

@ melpomene, так что else do; while (0)?
Карл Лей

8

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

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

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


Обычный шаблон заключается в добавлении подчеркивания в имена переменных в макросах - например int __i;.
Blaisorblade

8
@Blaisorblade: На самом деле это неправильно и незаконно C; ведущие подчеркивания зарезервированы для использования реализацией. Причина, по которой вы видели этот «обычный шаблон», связана с чтением системных заголовков («реализация»), которые должны ограничивать себя этим зарезервированным пространством имен. Для приложений / библиотек вы должны выбрать свои собственные непонятные, маловероятные имена без подчеркиваний, например mylib_internal___iили аналогичные.
R .. GitHub ОСТАНОВИТЬ ЛЬДА

2
@R .. Вы правы - на самом деле я читал это в «приложении», ядре Linux, но в любом случае это исключение, так как он не использует стандартную библиотеку (технически вместо этого «автономная» реализация C) '' размещенный '').
Blaisorblade

3
@R .. это не совсем правильно: начальные подчеркивания, за которыми следует заглавная или вторая , зарезервированы для реализации во всех контекстах. Подчеркивание, сопровождаемое чем-то другим, не зарезервировано в локальной области видимости.
Леушенко

@Leushenko: Да, но различие достаточно тонкое, поэтому я считаю, что лучше всего сказать людям, чтобы они вообще не использовали такие имена. Люди, которые понимают тонкость, по-видимому, уже знают, что я приукрашиваю детали. :-)
R .. GitHub ОСТАНОВИТЬСЯ ПОМОЩЬ ЛЬДУ

7

объяснение

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

3
использование блокировки блока {} в макросе было бы достаточно для связывания кода макроса, чтобы все выполнялось по одному и тому же пути if-условия. do-while around используется для принудительного использования точки с запятой в местах, где используется макрос. таким образом, макрос принудительно ведет себя как функция. это включает в себя требование запятой через запятую при использовании.
Александр Стор

2

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

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

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

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

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


15
Это не имеет ничего общего с конструкцией do ... while (0).
Трент

2
Правда. Но лучше поговорить о теме макросов и функций и о том, как написать макрос, который ведет себя как функция ...
Джон Нильссон,

2
Как и выше, это не ответ, а комментарий. По теме: вот почему вы используете материал только один раз:do { int macroname_i = (i); f(macroname_i); g(macroname_i); } while (/* CONSTCOND */ 0)
mirabilos
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.