Введение
Прежде чем компилятор увидит ваш код, его обрабатывает препроцессор. Он подставляет содержимое заголовков, раскрывает макросы и убирает лишние участки кода. Управляют этим препроцессорные директивы. Это команды, которые начинаются со знака # и занимают отдельную строку. В статье собран обзор основных директив C и C++ с примерами.
Как работают препроцессорные директивы
Сборка программы на C++ идёт в несколько этапов. Первый этап — препроцессор. Он читает исходный файл строка за строкой. Обычный код он не трогает. А строки, которые начинаются с решётки, он выполняет как команды.
Результат работы препроцессора — один большой текстовый файл. В нём уже нет директив и макросов. Именно этот текст и получает компилятор. Поэтому ошибка в макросе часто выглядит странно: компилятор показывает уже раскрытый код.
Посмотреть результат можно самому. В GCC и Clang для этого есть флаг -E, в MSVC — флаг /P. Это хороший способ понять, во что превращается сложный макрос.
#define — объявление макроса
Директива #define заменяет идентификатор на заданную последовательность символов везде, где он встречается дальше в файле:
#define BUFFER_SIZE 1024
char buffer[BUFFER_SIZE]; // после препроцессора: char buffer[1024];
Макросы могут принимать параметры:
#define MAX(x, y) ((x) > (y) ? (x) : (y))
int value = MAX(1, 4);
// после препроцессора:
int value = ((1) > (4) ? (1) : (4));
Скобки вокруг параметров и всего выражения обязательны. Без них приоритет операций может дать неожиданный результат. Длинный макрос можно перенести на следующую строку. Для этого в конце строки ставят обратную косую черту \:
#define LOG_ERROR(msg) \
qCritical() << __FILE__ << __LINE__ << msg
В современном C++ вместо макросов-констант лучше использовать constexpr, а вместо макросов-функций — шаблоны или inline-функции. Подробнее о макросах и их подводных камнях читайте в статье Препроцессор: #define, макросы, условная компиляция.
Операторы # и ##
Внутри макроса оператор # превращает параметр в строку, а ## склеивает два токена:
#define TO_STRING(x) #x
#define MAKE_VAR(name, n) name##n
const char *s = TO_STRING(hello); // "hello"
int MAKE_VAR(value, 1) = 10; // int value1 = 10;
#undef — отмена макроса
Директива #undef отменяет ранее объявленный макрос. После неё идентификатор можно объявить заново с другим значением:
#define BUFFER_SIZE 1024
// ...
#undef BUFFER_SIZE
#define BUFFER_SIZE 4096
Если переопределить макрос без #undef, компилятор выдаст предупреждение. Никогда не переопределяйте стандартные макросы вроде NULL: это ломает чужой код и библиотеки.
#include — подключение файлов
Директива #include вставляет текст другого файла в место вызова. Так подключаются объявления функций и классов:
#include <QDebug> // системные и библиотечные заголовки
#include "mainwindow.h" // заголовки вашего проекта
Угловые скобки означают, что файл ищется в системных каталогах. Кавычки — что поиск начнётся с каталога текущего файла. Чтобы заголовок не подключался дважды, используют include guards или #pragma once. Подробнее об этом — в статье Заголовочные файлы и include guards.
Условная компиляция: #if, #ifdef, #ifndef, #elif, #else, #endif
Условные директивы решают, какие участки кода попадут в компиляцию. Так пишут код для разных платформ и отладочный вывод:
#define DEBUG_LEVEL 2
#if DEBUG_LEVEL >= 2
qDebug() << "Подробный вывод";
#elif DEBUG_LEVEL == 1
qDebug() << "Краткий вывод";
#else
// вывод отключён
#endif
Директивы #ifdef и #ifndef проверяют, объявлен ли макрос, а оператор defined() позволяет комбинировать проверки:
#ifdef Q_OS_WIN
#include <windows.h>
#endif
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
// код только для настольного Linux
#endif
В C++17 появилась проверка __has_include, которая позволяет узнать, доступен ли заголовок:
#if __has_include(<optional>)
#include <optional>
#endif
#error и #warning — сообщения компилятора
Директива #error останавливает компиляцию с указанным сообщением. Она полезна для проверки обязательных настроек:
#if !defined(Q_OS_WIN)
#error "Этот модуль поддерживается только в Windows"
#endif
#if __cplusplus < 201703L
#error "Требуется C++17 или новее"
#endif
Директива #warning выводит предупреждение, но не прерывает сборку. Она давно поддерживается GCC и Clang и вошла в стандарт C++23.
#pragma — указания компилятору
Директива #pragma передаёт компилятору специальные инструкции. Их набор зависит от компилятора. Самые распространённые:
#pragma once— подключать заголовок только один раз;#pragma pack(push, 1)/#pragma pack(pop)— выравнивание полей структур;#pragma warning(disable : 4996)— отключить предупреждение в MSVC;#pragma message("текст")— вывести сообщение при компиляции.
Предопределённые макросы
Компилятор автоматически объявляет ряд полезных макросов:
__FILE__— имя текущего файла;__LINE__— номер текущей строки;__DATE__и__TIME__— дата и время компиляции;__cplusplus— версия стандарта C++ (например, 201703L для C++17);_MSC_VER,__GNUC__,__clang__— признаки конкретного компилятора.
qDebug() << "Ошибка в" << __FILE__ << "строка" << __LINE__;
qDebug() << "Собрано:" << __DATE__ << __TIME__;
Заключение
Препроцессорные директивы управляют тем, какой код увидит компилятор. #include подключает заголовки. #define и #undef объявляют и отменяют макросы. Условные директивы выбирают нужные участки кода. #error и #pragma управляют самой сборкой. Для констант и функций лучше использовать средства языка C++.