Секрет C++ макросов: зачем нужны do-while(0) и if-else в макросах и 3 фатальные ошибки

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Макросы в C++: опасная красота или необходимое зло?

Каждый, кто хоть раз писал на C++, сталкивался с макросами. С одной стороны — это мощный инструмент для метапрограммирования, который может сократить количество шаблонного кода. С другой — макросы славятся своей коварностью. Достаточно пропустить одну точку с запятой или неправильно расставить скобки, и код превращается в непредсказуемого монстра.

Но есть в мире C++ особая категория макросов, которая выглядит откровенно странно. Вы когда-нибудь видели такой код?

#define SAFE_DELETE(ptr) do { delete (ptr); (ptr) = nullptr; } while(0)

Вроде бы логично: удалить указатель и занулить его. Но зачем здесь этот бессмысленный do-while(0)? Цикл, который гарантированно выполняется ровно один раз? Выглядит как избыточность, граничащая с абсурдом. Или вот ещё пример с if-else:

#define LOG_ERROR(msg) if (g_logging) std::cerr << (msg)

Кажется, что всё просто. Но именно здесь скрываются грабли, о которые споткнулись тысячи разработчиков. Этот вопрос на Stack Overflow собрал десятки тысяч просмотров, и сегодня мы разберём, почему такие конструкции — не баг, а фича, и какие ошибки макросов C++ они предотвращают.

Почему макросы — это не функции? Главная проблема

Прежде чем нырять в дебри do-while(0), давайте вспомним, чем макрос отличается от обычной функции. В C++ макрос — это текстовая подстановка. Препроцессор просто копирует тело макроса в то место, где он вызван. Никаких проверок типов, никаких областей видимости — чистая замена строк.

Именно из-за этого возникают классические ошибки в макросах C++. Рассмотрим простой пример:

#define SQUARE(x) x * x

int main() {
    int a = 5;
    int result = SQUARE(a + 1); // Ожидаем 36, получаем 11
    std::cout << result; // Выведет 11
    return 0;
}

Что произошло? Макрос развернулся в a + 1 * a + 1. Из-за приоритета операторов умножение выполнилось раньше сложения. Первая ошибка — отсутствие скобок. Исправим:

#define SQUARE(x) ((x) * (x))

Теперь работает. Но это только цветочки. Настоящие проблемы начинаются, когда макрос содержит несколько операторов или управляющие конструкции.

Идиома do-while(0): щит от синтаксических катастроф

Вернёмся к нашему SAFE_DELETE. Почему бы не написать макрос проще, без этого странного цикла?

#define SAFE_DELETE(ptr) delete (ptr); (ptr) = nullptr;

Кажется логичным. Но представьте, что вы используете его в таком контексте:

if (ptr != nullptr)
    SAFE_DELETE(ptr);
else
    std::cout << "Pointer is null";

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

if (ptr != nullptr)
    delete (ptr);
    (ptr) = nullptr;
else
    std::cout << "Pointer is null";

И вот он — баг! else теперь не относится к if, потому что между ними появился второй оператор. Компилятор выдаст ошибку, но суть ясна: макрос «сломал» управляющую конструкцию. Именно для таких случаев и нужен do-while(0). Он превращает несколько операторов в один составной, сохраняя логику:

if (ptr != nullptr)
    do { delete (ptr); (ptr) = nullptr; } while(0);
else
    std::cout << "Pointer is null";

Теперь всё работает корректно. Идиома макросов do-while(0) — это стандартный приём, который гарантирует, что макрос будет вести себя как единый оператор.

Почему именно do-while(0), а не просто блок { }?

Хороший вопрос! Ведь можно было бы использовать просто фигурные скобки:

#define SAFE_DELETE(ptr) { delete (ptr); (ptr) = nullptr; }

Попробуем использовать такой макрос:

if (ptr != nullptr)
    SAFE_DELETE(ptr);
else
    std::cout << "Pointer is null";

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

if (ptr != nullptr)
    { delete (ptr); (ptr) = nullptr; };
else
    std::cout << "Pointer is null";

Видите точку с запятой после закрывающей скобки? Это отдельный пустой оператор. Компилятор C++ воспримет это как конец if, а else останется «висеть в воздухе». Опять ошибка! Конструкция do { ... } while(0) заканчивается точкой с запятой естественным образом, и никаких проблем не возникает.

Это одна из самых частых ошибок в макросах C++, которую допускают новички. Если вы пишете макрос с несколькими операторами — всегда используйте do-while(0).

Макросы с if-else: как не сломать логику программы

Вторая популярная ловушка — макросы, которые сами содержат if-else. Вспомним наш LOG_ERROR:

#define LOG_ERROR(msg) if (g_logging) std::cerr << (msg)

Используем его в коде:

if (error_occurred)
    LOG_ERROR("Error happened");
else
    process_data();

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

if (error_occurred)
    if (g_logging) std::cerr << "Error happened";
else
    process_data();

Теперь else относится к внутреннему if, а не к внешнему! Это классическая проблема «висячего else». В результате process_data() будет вызываться, когда error_occurred истинно, но g_logging ложно — полная противоположность ожидаемому поведению.

Как исправить? Есть два пути:

Путь первый: оборачиваем в do-while(0)

#define LOG_ERROR(msg) do { if (g_logging) std::cerr << (msg); } while(0)

Теперь макрос — это единый оператор, и else привязывается к внешнему if корректно.

Блоги

Рекомендаций по книгам