Avatar ·

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

Привет! Я начал изучать C++ и наткнулся на странный паттерн в макросах. Часто вижу в коде библиотек (например, в Qt или Boost) такие конструкции: ```cpp #define LOG_ERROR(msg) do { \ std::cerr << "ERROR: " << msg << std::endl; \ } while(0) ``` Или: ```cpp #define SAFE_DELETE(p) if(p) { delete p; p = nullptr; } else (void)0 ``` На первый взгляд, это выглядит абсолютно бессмысленно. Зачем писать `do { ... } while(0)`, если цикл выполняется ровно один раз? И почему бы не написать просто: ```cpp #define LOG_ERROR(msg) std::cerr << "ERROR: " << msg << std::endl ``` Я попробовал использовать простой макрос без do-while, и он сработал в простом случае. Но потом я вставил его в условие if-else, и код сломался. Например: ```cpp #define LOG_ERROR(msg) std::cerr << "ERROR: " << msg << std::endl if (error) LOG_ERROR("Something went wrong"); else std::cout << "OK" << std::endl; ``` Компилятор выдал ошибку или странное поведение. Можете объяснить, зачем нужны эти do-while и if-else в макросах, и как правильно их писать? И почему просто не использовать inline-функции? <<>>

Проблема: макросы и синтаксический контекст

Вы столкнулись с классической проблемой макросов в C/C++ — они работают как простая текстовая подстановка, не учитывая контекст использования. Рассмотрим ваш пример:

#define LOG_ERROR(msg) std::cerr << "ERROR: " << msg << std::endl

if (error)
    LOG_ERROR("Something went wrong");
else
    std::cout << "OK" << std::endl;

После препроцессинга этот код превращается в:

if (error)
    std::cerr << "ERROR: " << "Something went wrong" << std::endl;
else
    std::cout << "OK" << std::endl;

Здесь всё работает корректно, потому что макрос состоит из одного выражения (оператора <<). Но что если макрос содержит несколько выражений? Например:

#define LOG_AND_ABORT(msg) \
    std::cerr << "ERROR: " << msg << std::endl; \
    std::abort()

if (error)
    LOG_AND_ABORT("Fatal error");
else
    std::cout << "OK" << std::endl;

После подстановки получаем:

if (error)
    std::cerr << "ERROR: " << "Fatal error" << std::endl;
    std::abort();  // <-- Эта строка ВНЕ блока if!
else
    std::cout << "OK" << std::endl;

Код не скомпилируется, потому что else теперь не относится к if — между ними оказался std::abort(). Даже если убрать else, std::abort() будет выполняться всегда, а не только при ошибке.

Решение: do { ... } while(0)

Конструкция do { ... } while(0) решает эту проблему. Она:

  • Группирует несколько выражений в одно — всё, что внутри блока, становится единым составным оператором
  • Требует точку с запятой в конце — макрос выглядит как обычный вызов функции
  • Не влияет на окружающий код — цикл выполняется ровно один раз
#define LOG_AND_ABORT(msg) do { \
    std::cerr << "ERROR: " << msg << std::endl; \
    std::abort(); \
} while(0)

if (error)
    LOG_AND_ABORT("Fatal error");  // Точка с запятой обязательна
else
    std::cout << "OK" << std::endl;

После подстановки:

if (error)
    do {
        std::cerr << "ERROR: " << "Fatal error" << std::endl;
        std::abort();
    } while(0);  // Это единый оператор
else
    std::cout << "OK" << std::endl;

Теперь всё корректно: do-while — это один оператор, и else правильно привязывается к if.

Почему не if-else?

Иногда используют if (1) { ... } else (void)0:

#define SAFE_DELETE(p) if(p) { delete p; p = nullptr; } else (void)0

Это тоже работает, но имеет недостаток: если забыть точку с запятой, компилятор может не заметить ошибку. do-while гарантирует, что точка с запятой обязательна.

Почему не inline-функции?

Вы абсолютно правы: в современном C++ макросы для таких целей — зло. Надо использовать inline-функции или шаблоны:

template<typename T>
inline void log_error(const T& msg) {
    std::cerr << "ERROR: " << msg << std::endl;
}

Однако макросы всё ещё нужны в трёх случаях:

  • Когда нужно использовать __FILE__, __LINE__ (имя файла и номер строки)
  • Когда нужно создавать идентификаторы через ## (конкатенация токенов)
  • В условной компиляции (#ifdef)

Для всего остального — используйте современные возможности C++.

<<>>

Альтернатива: завернуть в фигурные скобки

Иногда можно обойтись простым блоком в фигурных скобках { ... }, но это работает не всегда. Рассмотрим:

#define LOG_AND_ABORT(msg) { \
    std::cerr << "ERROR: " << msg << std::endl; \
    std::abort(); \
}

if (error)
    LOG_AND_ABORT("Fatal error");
else
    std::cout << "OK" << std::endl;

После подстановки:

if (error)
    {
        std::cerr << "ERROR: " << "Fatal error" << std::endl;
        std::abort();
    };  // <-- Точка с запятой создаёт пустой оператор!
else
    std::cout << "OK" << std::endl;

Компилятор увидит if (error) { ... }; — блок с точкой с запятой воспринимается как два оператора: блок и пустой оператор. else остаётся без if → ошибка компиляции.

do { ... } while(0) решает эту проблему, потому что точка с запятой является частью синтаксиса do-while, а не лишним оператором.