C++ Компиляция. Препроцессор.

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 в отдельном файле. 

Пример main.cc:

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).

main.cc:

#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 – соответствующий код.

Я хотел уместить в эту заметку так же информацию про “предкомпилированные заголовки”, однако заметка и так получилась сильно больше чем я хотел, так что по ним я сделаю отдельную заметку.



Report Page