Условная компиляция позволяет одному файлу исходника становиться несколькими разными программами. Препроцессор решает ещё до запуска компилятора, какие блоки текста выживут, — поэтому код внутри неудавшейся ветки не просто пропускается во время выполнения, он удаляется. Он никогда не разбирается, поэтому ничего не добавляет к исполняемому файлу и может даже содержать конструкции, которые текущий компилятор отверг бы.
Вот чем это отличается от обычного if. Оба выбирают между путями; только один из них выбирает до того, как программа появилась на свет.
#ifdef и #ifndef
Самый простой вопрос — определён ли макрос вообще:
Удалите строку #define DEBUG, и каждый отладочный вывод исчезнет из сборки — не отключится, а исчезнет. Запустите программу как есть, затем уберите эту строку и запустите снова.
#ifndef — отрицание: сохранить блок, только если макрос не определён. Чаще всего он применяется в защите от повторного включения, описанной в заголовочных файлах, а ещё так задают значение по умолчанию, которое вызывающая сторона может переопределить:
Каждый #ifdef и #ifndef должен закрываться #endif. Подпись у закрывающей директивы в длинных файлах — #endif /* DEBUG */ — экономит потом уйму времени.
#if, #elif, #else
#if принимает константное целочисленное выражение и сохраняет блок, когда оно ненулевое. Это делает возможными сравнения версий и уровней:
Поменяйте LOG_LEVEL на 0 или 3 и запустите снова, чтобы увидеть, как сама программа меняет форму.
Выражение вычисляет препроцессор, а значит, в нём допустимы только целочисленные константы, арифметические операторы, операторы сравнения и макросы, раскрывающиеся в них. Он не видит sizeof, значений enum, переменных и вообще ничего, что требует компилятора. Неопределённый макрос внутри #if вычисляется в 0, а не считается ошибкой, — это удобно и время от времени неожиданно:
#if FEATURE_X /* FEATURE_X нигде не определён -> 0 -> блок выброшен */
defined(ИМЯ) превращает вопрос #ifdef в нечто пригодное для использования внутри #if, так что проверки можно комбинировать:
#if defined(DEBUG) && !defined(NDEBUG)
/* отладочная сборка, и проверки assert включены */
#endif
#if defined(LINUX) || defined(BSD)
/* любая unix-подобная система */
#endif
#if defined(X) и #ifdef X означают одно и то же; берите первое, когда нужны &&, || или !.
Определение макросов из командной строки
Настоящая сила этих переключателей в том, что исходник вообще не обязан меняться. gcc -D определяет макрос для всей компиляции:
gcc -DDEBUG program.c -o program # DEBUG определён как 1
gcc -DLOG_LEVEL=3 program.c -o program # конкретное значение
gcc -DDEBUG -DBUFFER_SIZE=512 a.c b.c -o app
-DИМЯ без значения равносильно #define ИМЯ 1. -U ИМЯ отменяет определение — это важно, когда заголовочный файл определяет что-то, что вам нужно выключить.
Поэтому обычная схема такова: в исходнике лежат значения по умолчанию под защитой #ifndef, а команда сборки выбирает конфигурацию.
/* config.h */
#ifndef LOG_LEVEL
#define LOG_LEVEL 1 /* по умолчанию тихо */
#endif
#ifndef MAX_CONNECTIONS
#define MAX_CONNECTIONS 64
#endif
# сборка для разработки
gcc -DDEBUG -DLOG_LEVEL=3 -Wall -Wextra -g src/*.c -o app-dev
# релизная сборка
gcc -DNDEBUG -O2 src/*.c -o app
NDEBUG стандартизован: его определение отключает каждый assert() в программе, потому что сам <assert.h> написан с использованием условной компиляции. Это тот же приём в миниатюре — заголовочный файл, который компилируется в разное в зависимости от того, что определила сборка.
Переключатели платформы
Компиляторы предопределяют макросы, идентифицирующие целевую систему, поэтому один файл исходника может вызывать на каждой из них свой API:
#if defined(_WIN32)
#include <windows.h>
#define CLEAR_SCREEN "cls"
#elif defined(__APPLE__)
#include <unistd.h>
#define CLEAR_SCREEN "clear"
#elif defined(__linux__)
#include <unistd.h>
#define CLEAR_SCREEN "clear"
#else
#error "Unsupported platform"
#endif
Самые ходовые: _WIN32 (определён и на 32-, и на 64-битной Windows), _WIN64, __linux__, __APPLE__, __unix__, __ANDROID__. У самого компилятора свой набор — __GNUC__, __clang__, _MSC_VER, — и у архитектуры тоже: __x86_64__, __aarch64__.
Запускаемая версия, которая сообщает, на чём её собрали:
#error стоит знать отдельно: он останавливает компиляцию с вашим сообщением. Завершение цепочки платформ через #else / #error "Unsupported platform" превращает молчаливую неправильную сборку во внятный сбой в самом верху журнала сборки.
Чтобы вывести все макросы, которые предопределяет ваш компилятор:
gcc -dM -E - < /dev/null
Этот вывод — авторитетный ответ на вопрос «что вообще можно проверять на этой машине».
#if 0 для отключения кода
Закомментирование блока через /* ... */ ломается в тот момент, когда внутри блока оказывается собственный комментарий, потому что комментарии C не вкладываются: первый же */ внутри завершает внешний комментарий, и всё после него становится бесхозным кодом. У #if 0 такой проблемы нет:
#if 0
/* Весь этот участок удаляется, вместе с комментариями. */
legacy_init();
int n = old_calculation(42); /* и этот комментарий не помеха */
report(n);
#endif
Замените на #if 1, чтобы вернуть код. Редакторы продолжают подсвечивать его как C, а #if 0 можно вложить внутрь другого #if.
Это инструмент отладки, а не хранилище. Код, лежащий в #if 0, никогда не компилируется, поэтому тихо гниёт: к тому моменту, когда кто-нибудь щёлкнет переключателем, он уже не соберётся. Пользуйтесь этим, пока локализуете проблему, а потом удаляйте блок и доверьте память системе контроля версий.
Где условная компиляция даёт сбой
Основную часть боли объясняют три сценария.
Код, который никогда не компилируется, никогда и не проверяется. Опечатка внутри неактивной ветки #ifdef невидима, пока кто-нибудь не соберёт эту конфигурацию — возможно, через несколько месяцев, возможно, в CI на платформе, которой у вас нет. Если в проекте есть нетривиальные ветки под платформы, собирайте их все регулярно.
Переплетение #ifdef с потоком управления очень быстро становится нечитаемым. О такой конструкции трудно рассуждать:
if (ready) {
#ifdef FAST_PATH
fast_send(buf);
} else {
#endif
slow_send(buf);
}
Скобки, открытые в одной ветке и закрытые в другой, законны и ужасны. Лучше оборачивать условиями функции целиком и выбирать между ними:
#ifdef FAST_PATH
static void send_data(const char *buf) { fast_send(buf); }
#else
static void send_data(const char *buf) { slow_send(buf); }
#endif
Обычный if времени выполнения часто лучше. Если обе ветки компилируются везде, обычный if (debug_enabled) сохраняет оба пути проверенными по типам, тестируемыми и переключаемыми без пересборки. Оставьте препроцессору то, что умеет только он: код, который действительно не может скомпилироваться на другой платформе.
Приём, объединяющий всё это, — заголовочный файл со значениями по умолчанию, -D для конкретной сборки и ветки под платформы внутри защиты от повторного включения — смотрите в заголовочных файлах; про сам #define — в макросах.
Часто задаваемые вопросы
Что такое условная компиляция в C?
Это использование директив препроцессора, чтобы сохранить или выбросить блоки исходного кода до запуска компилятора. Код внутри неудавшегося #ifdef полностью удаляется из текста: он никогда не разбирается, не компилируется и не попадает в исполняемый файл. Так один файл исходника поддерживает несколько платформ или типов сборки.
В чём разница между #ifdef и #if?
#ifdef ИМЯ спрашивает только о том, существует ли макрос, независимо от его значения. #if выражение вычисляет константное целочисленное выражение, поэтому умеет сравнивать значения: #if VERSION >= 3. Используйте #ifdef для флагов, которые просто есть или которых нет, и #if, когда важно значение. #if defined(ИМЯ) совмещает оба варианта и сочетается с && и ||.
Как определить макрос из командной строки в gcc?
Через -D: gcc -DDEBUG program.c определяет DEBUG так, будто файл начинался с #define DEBUG 1, а -DMAX=50 задаёт конкретное значение. Именно так системы сборки включают возможности, не правя исходник; -U ИМЯ отменяет определение.
Почему стоит использовать #if 0 вместо комментирования кода?
Потому что комментарии /* ... */ не вкладываются: первый же */ внутри участка досрочно завершает комментарий, и остаток превращается в сломанный код. #if 0 ... #endif убирает любой объём кода с любым количеством комментариев внутри, сохраняет подсветку синтаксиса и элементарно возвращается обратно заменой на #if 1.