Зачем в макросах C++ используют бессмысленные do-while и if-else?
Проблема: макросы и синтаксический контекст
Вы столкнулись с классической проблемой макросов в 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, а не лишним оператором.