9

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

5

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

5 ответ(ов)

1

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

Вот три совета, как добиться этого:

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

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

doSomething(1);
DO_SOMETHING_ELSE(2);  // <== Эй? Что это?
doSomethingElseAgain(3);

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

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

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

Генерируйте корректный код

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

if(bIsOk)
   MY_MACRO(42);

Этот макрос может расширяться так:

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

if(bIsOk)
   f(42); g(42); // это был MY_MACRO(42);

Функция g будет выполнена независимо от значения bIsOk.

Это означает, что нам необходимо создать область видимости для макроса:

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

if(bIsOk)
   { f(42); g(42); } // это был 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); // это был MY_MACRO(32);
}

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

#define MY_MACRO(x) { int i = x + 1; f(i); }

void doSomething()
{
    int i = 25;
    { int i = 32 + 1; f(i); } // это был MY_MACRO(32);
}

Код снова ведет себя корректно.

Сочетание эффектов точки с запятой и области видимости?

Существует один идиоматический прием в C/C++, который дает этот эффект: цикл do/while:

do
{
    // код
}
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);

   // И так далее.
}

правильно развернется как

void doSomething(bool bIsOk)
{
   int i = 25;

   if(bIsOk)
      do
      {
         const int i = 42 + 1; // это был MY_MACRO(42);
         f(i); g(i);
      }
      while(false);

   // И так далее.
}

и затем будет скомпонован и оптимизирован как

void doSomething(bool bIsOk)
{
   int i = 25;

   if(bIsOk)
   {
      f(43); g(43);
   }

   // И так далее.
}
0

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

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

if (test) FOO(baz);

при разворачивании превратится в:

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

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

0

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

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

FOO(1)
printf("abc");

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

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

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

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

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.

0

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

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

Поэтому никогда не используйте распространенные имена переменных в макросах!

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь