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

10 ответов:

The do ... while и if ... else есть сделать так, что точка с запятой после макроса всегда означает одно и то же. Скажем, вы было что-то вроде вашего второго макроса.

#define BAR(X) f(x); g(x)

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

есть три хороших "подсказки", чтобы преуспеть в этом:

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

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

doSomething(1) ;
DO_SOMETHING_ELSE(2)  // <== Hey? What's this?
doSomethingElseAgain(3) ;

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

но реальная реальная причина заключается в том, что в какой-то момент автору макроса, возможно, потребуется заменить макрос подлинной функцией (возможно, встроенной). Так что макрос должен действительно ведут себя как один.

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

создать действительный код

как показано в ответе jfm3, иногда макрос содержит более одной инструкции. И если макрос используется внутри если заявление, это будет проблематично:

if(bIsOk)
   MY_MACRO(42) ;

этот макрос может быть расширена как:

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

if(bIsOk)
   f(42) ; g(42) ; // was MY_MACRO(42) ;

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

@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 строить.

Йенс 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, проверьте эту библиотеку.

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

некоторые из вас показывали макросы с локальными переменными, но никто не упоминал, что вы не можете просто использовать любое имя в макросе! Он будет кусать пользователя когда-нибудь! Зачем? Потому что входные аргументы подставляются в шаблон макроса. И в примерах макросов вы используете, вероятно, наиболее часто используемое переменное имя Я.

например, когда следующий макрос

#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(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);

объяснение

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

Comments

    Ничего не найдено.