C++ Компиляция. Препроцессор.
patriosЕсли вы хотя бы немного программировали на C++ вы, скорее всего, замечали в проекте файлы с расширением “.h” иначе называемые “заголовочными” файлам. Однако, как я уже писал в заметке по основам компиляции – непосредственно в процессе сборки эти файлы не используются. Куда же они деваются?
Включением данных файлов, как и рядом других дополнительных операций проводимых над файлом с исходниками, занимается механизм называемый “препроцессор”.
Инструкции которыми занимается препроцессор начинаются с символа #. Основные инструкции препроцессора: #define, Условные директивы (#if|#ifdef|#ifnder|#endif|#else), #include, #pragma. Мы рассмотрим их подробнее с примерами ниже. Все исходники можно найти тут.

Это все интересно, но все таки зачем нам нужны заголовочные файлы?
Давайте рассмотрим пример в котором мы хотим реализовать функции в одном модуле “math.cc”, а использовать в другом “main.cc”
math.h
int sum(int value1, int value2) {
return value1 + value2;
}
int mult(int value1, int value2) {
return value1 * value2;
}
main.cc:
int sum(int value1, int value2);
int mult(int value1, int value2);
int main() {
return mult(sum(1, 2), 3);
}
Как я уже писал в обзорной статьи – каждый модуль трансляции компилируется отдельно, следовательно в рамках одного модуля компилятор должен знать о всех функциях и классах которые определены в других модулях. Для этого нам нужно предоставить компилятору сигнатуру этих методов перед их использованием. В данном примере это значит, что перед использованием методов mult и sum нам необходимо объявить их в модуле main.cc:
int sum(int value1, int value2); int mult(int value1, int value2);
Более того данное объявление придется повторять во всех модулях которые используют модуль math.cc.
Собственно чтобы автоматизировать данную операции в C++ принято объявлять все методы реализованные в конкретном модуле в отдельном “заголовочном” файле и включать его в местах использования директивой #include.
Давайте теперь вернемся к директивам препроцессора и рассмотрим их по отдельности:
#define – создает макрос ассоциированный с некоторым идентификатором. Знают это определение звучит сложно, но по факту когда вы видите в коде что то типа #define VALUE 3 – везде в дальнейшем кода на этапе препроцессинга слово “VALUE” будет заменяться значением 3.
#define может заменить идентификатор полноценным макросом например:
#define sum(f1,f2) (f1 + f2)
int main() {
return sum(1, 2);
}
В данном примере sum(1, 2) будет буквально заменено на 1 + 2 на этапе препроцессинга.
#include – буквально включает файл (по пути переданному в аргументе) в исходный код модуля по месту использования. Тут стоит понимать что с помощью операции #include можно включить буквально любой файл, не обязательно заголовочный. К примеру вы можете определить значения enum в отдельном файле.
enum Animals {
#include "animals.ii"
};
int main() {
return Dog;
}
animals.ii
Dog = 1, Cat = 2, Sheep = 3
Условные директивы типа #ifdef|#else – данные директивы позволяют включать в исходники на процессе препроцессинга куски кода только при определенных условиях. Самый простой пример – код зависящий от операционной системы. К примеру обработка разделителя в пути к директории:
int main() {
#ifdef __linux__
char delimiter = '/';
#elif _WIN32
char delimiter = '\\';
#else
exit(1);
#endif
return delimiter;
}
#pragma – платформо специфичные директивы. Данные директивы могут иметь разное применение в зависимости от платформы и компилятора. К примеру #pragma once которая часто выступает замещением #ifdef/#define/#endif при компиляции MSVC. Зачем вообще нужны эти операции в заголовочных файлах я распишу в отдельной заметке.
Давайте теперь рассмотрим результат препроцессинга всех этих директив (помимо pragma).
#include "math.h"
#define VALUE 3
#define MSUM(a) mult(sum(a, a), a)
int main() {
#ifdef COMPILE_FLAG
return MSUM(VALUE);
#else
return VALUE;
#endif
}
Что бы запустить только препроцессор мы можем передать компилятору параметр "-E" далее результирующий файл будет выведен на экран. Его так же можно будет сохранить в файл флагом "-o". Пример команды:
clang++ -E main.cc -o main.i
Содержимое main.i:
# 1 "main.cc"
…
# 1 "main.cc" 2
# 1 "./math.h" 1
int sum(int value1, int value2);
int mult(int value1, int value2);
# 2 "main.cc" 2
int main() {
return 3;
}
Как мы видим препроцессор включил в состав модуля файл math.h, выбрал ветку else и подставил вместо идентификатора VALUE значение 3.
Давайте теперь на этапе компиляции определим флаг COMPILE_FLAG командой:
clang++ -D COMPILE_FLAG -E main.cc -o main.i
Содержимое main.i:
# 1 "main.cc"
…
# 1 "./math.h" 1
int sum(int value1, int value2);
int mult(int value1, int value2);
# 2 "main.cc" 2
int main() {
return mult(sum(3, 3), 3);
}
Как видно препроцессор теперь выбрал ветку #if и подставил вместо идентификатора MSUM – соответствующий код.
Я хотел уместить в эту заметку так же информацию про “предкомпилированные заголовки”, однако заметка и так получилась сильно больше чем я хотел, так что по ним я сделаю отдельную заметку.