# Глава 10. Структура программы
Проект на C++ может быть сколь угодно сложным и содержащим произвольную иерархию директорий. Он почти всегда включает специфичные для системы сборки скрипты, конфиги и другие вспомогательные файлы. Но в этой главе нас интересуют именно файлы с кодом на C++. Мы разберем, по какому принципу они составляются. А в следующей главе мы научимся собирать проект.
## Объявления и определения
Функции, классы, переменные и другие сущности в программе становятся видны компилятору после точки своего объявления. Если использовать их до места в коде, где они объявлены, то компилятор выдаст ошибку.
Убедимся в этом на примере, в котором есть [взаимная рекурсия.](https://ru.wikipedia.org/wiki/%D0%92%D0%B7%D0%B0%D0%B8%D0%BC%D0%BD%D0%B0%D1%8F_%D1%80%D0%B5%D0%BA%D1%83%D1%80%D1%81%D0%B8%D1%8F) Пример реализует [гипотезу Коллатца:](https://ru.wikipedia.org/wiki/%D0%93%D0%B8%D0%BF%D0%BE%D1%82%D0%B5%D0%B7%D0%B0_%D0%9A%D0%BE%D0%BB%D0%BB%D0%B0%D1%82%D1%86%D0%B0) какое бы натуральное число `n` мы ни взяли, рано или поздно мы получим единицу, если будем совершать действия:
- Если `n` четное, делить его на 2.
- Если `n` нечетное, умножать на 3 и прибавлять 1.
```c++ {.example_for_playground}
import std;
int collatz_multiply(int x)
{
return (x % 2 > 0) ? 3 * x + 1 : collatz_divide(x);
}
int collatz_divide(int x)
{
return (x % 2 == 0) ? x / 2 : collatz_multiply(x);
}
int main()
{
int n = 17;
std::println("Checking Collatz conjecture for {}", n);
while (n > 1)
{
n = collatz_multiply(n);
std::print("{} ", n);
}
}
```
```
main.cpp:5:36: error: use of undeclared identifier 'collatz_divide'
5 | return x % 2 > 0 ? 3 * x + 1 : collatz_divide(x);
| ^
```
Компилятор не нашел функцию, вызванную до своего объявления. Поправить это простым переносом функции выше места ее вызова не получится. Ведь `collatz_multiply()` вызывает `collatz_divide()` и наоборот! Кроме того, код может быть достаточно сложным, чтобы подходящего места для размещения всех функций до их вызовов просто бы не нашлось.
На помощь приходят [объявления](https://en.cppreference.com/w/cpp/language/declarations.html) (declarations). Объявление делает сущность видимой для компилятора.
Объявление функции состоит из возвращаемого типа, имени функции и параметров. После объявления ставится оператор `;`.
```c++
int collatz_multiply(int x);
```
Все функции, классы, структуры и перечисления, с которыми вы успели поработать в предыдущих главах, являются _определениями._ Единственное исключение — объявление функции `read_file()` в файле `main.cpp` [предыдущей практики](/courses/cpp/practice/cpp_brainfuck_interpreter/) «Интерпретатор Brainfuck».
[Определение](https://en.cppreference.com/w/cpp/language/definition.html) (definition) — это объявление вместе с информацией, которой достаточно для использования сущности в коде. Определение функции включает и ее тело, то есть реализацию.
Любое определение также является и объявлением.
Чтобы исправить пример кода с гипотезой Коллатца, разместим объявления функций до их использования:
```c++ {.example_for_playground}
import std;
int collatz_multiply(int x);
int collatz_divide(int x);
int main()
{
int n = 17;
std::println("Checking Collatz conjecture for {}", n);
while (n > 1)
{
n = collatz_multiply(n);
std::print("{} ", n);
}
}
int collatz_multiply(int x)
{
return (x % 2 > 0) ? 3 * x + 1 : collatz_divide(x);
}
int collatz_divide(int x)
{
return (x % 2 == 0) ? x / 2 : collatz_multiply(x);
}
```
```
Checking Collatz conjecture for 17
52 26 13 40 20 10 5 16 8 4 2 1
```
Объявлений одной и той же сущности в программе может быть сколь угодно много. Но определение должно быть единственным. Так гласит важный пункт из [правила одного определения](https://en.cppreference.com/w/cpp/language/definition) (ODR, one definition rule). Нарушение ODR приведет к ошибке компиляции.
Что выведет этот код? {.task_text}
В случае ошибки напишите `err`. {.task_text}
```c++ {.example_for_playground}
import std;
int main()
{
std::println("{}", to_miles(0.0));
}
double to_miles(double km)
{
return km * 0.62;
}
```
```consoleoutput {.task_source #cpp_chapter_0100_task_0010}
```
Функция `to_miles()` объявлена после ее вызова. {.task_hint}
```cpp {.task_answer}
err
```
Объявление класса не включает реализацию его методов:
```c++ {.example_for_playground .example_for_playground_001}
class Message
{
public:
Message(std::string raw_text);
std::string get_message();
std::time_t get_timestamp();
private:
std::string msg;
std::time_t ts;
};
```
Объявление класса в связке с реализацией методов считается определением класса. При реализации метода _вне_ тела класса перед методом указывается имя класса, отделенное от имени метода оператором разрешения области видимости `::`.
```c++ {.example_for_playground .example_for_playground_002}
Message::Message(std::string raw_text)
{
msg = parse_message(raw_text);
ts = parse_time(raw_text);
}
std::string Message::get_message()
{
return msg;
}
std::time_t Message::get_timestamp()
{
return ts;
}
```
Приемная комиссия колледжа принимает заявления на поступление вместе с результатами теста. Члены комиссии должны отслеживать `n`-ый наивысший балл по тестам. Это нужно делать в потоковом режиме, по мере того, как поступают новые заявления. Отслеживание `n`-ного наивысшего балла помогает определить пороговое значение, необходимое для зачисления `n` учеников. {.task_text}
Перед вами объявление класса `NthLargest`. Реализуйте определения методов. Они должны идти после объявления класса. {.task_text}
Конструктор принимает число `n` и значение по умолчанию, которое нужно возвращать, пока не накопится `n` результатов теста. {.task_text}
Метод `add()` добавляет новый результат теста и возвращает обновленный `n`-ый наивысший балл. Пока `n` результатов не накопилось, метод возвращает `default_val`. {.task_text}
В реализации используется [очередь с приоритетами.](/courses/cpp/chapters/cpp_chapter_0070/#block-priority-queue) {.task_text}
```c++ {.task_source #cpp_chapter_0100_task_0020}
class NthLargest
{
public:
NthLargest(std::size_t n, std::size_t default_val);
std::size_t add(std::size_t val);
private:
std::size_t m_n;
std::size_t m_default;
// https://en.cppreference.com/w/cpp/container/priority_queue
std::priority_queue<std::size_t, // тип элемента
std::vector<std::size_t>, // контейнер для адаптера
std::greater<std::size_t> // компаратор
> m_pq;
};
```
Очередь с приоритетами реализует структуру данных [куча](https://ru.wikipedia.org/wiki/%D0%9A%D1%83%D1%87%D0%B0_(%D1%81%D1%82%D1%80%D1%83%D0%BA%D1%82%D1%83%D1%80%D0%B0_%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85)) (heap). По умолчанию элемент с наибольшим значением ключа находится на вершине кучи. Чтобы на вершине оказался наименьший элемент, шаблонный класс `std::priority_queue` был инстанцирован компаратором `std::greater` вместо `std::less`. Вам осталось контроллировать количество элементов кучи. Оно не должно превосходить `n`. {.task_hint}
```c++ {.task_answer}
class NthLargest
{
public:
NthLargest(std::size_t n, std::size_t default_val);
std::size_t add(std::size_t val);
private:
std::size_t m_n;
std::size_t m_default;
// https://en.cppreference.com/w/cpp/container/priority_queue
std::priority_queue<std::size_t, // тип элемента
std::vector<std::size_t>, // контейнер для адаптера
std::greater<std::size_t> // компаратор
> m_pq;
};
NthLargest::NthLargest(std::size_t n, std::size_t default_val)
{
m_n = n;
m_default = default_val;
}
std::size_t NthLargest::add(std::size_t val)
{
if (m_pq.size() < m_n)
{
m_pq.push(val);
return (m_pq.size() < m_n) ? m_default : m_pq.top();
}
if (std::size_t cur_nth_largest = m_pq.top(); cur_nth_largest < val)
{
m_pq.push(val);
if (m_pq.size() > m_n)
m_pq.pop();
}
return m_pq.top();
}
```
Объявления и определения могут находиться в разных файлах проекта. Принципы их размещения зависят от того, как в проекте импортируются в код сущности: с помощью хедеров или модулей.
## Проекты до появления модулей
До C++20 проекты содержали файлы двух видов:
- Хедеры (headers) или заголовочные файлы. Им принято давать расширение `.h`, `.hpp` или `.hxx`.
- Файлы реализации. Но на практике вы вряд ли услышите это название. Обычно говорят «файлы исходников» или просто «`cpp`-файлы». Варианты расширений: `.cpp`, `.cxx`, `.cc`.
Хедеры содержат объявления, а `cpp`-файлы — определения. Но в некоторых случаях определения можно или даже нужно помещать в хедеры. Каждый из них мы рассмотрим отдельно. А пока помните: если допустимы оба варианта, всегда выбирайте `cpp`-файл.
Кроме того, расширение файла — всего лишь договоренность. Оно может быть любым. И все же давайте файлам одно из общепринятых расширений. Например, `.h` для хедеров и `.cpp` для файлов реализации.
В простейшем случае проект — это единственный `cpp`-файл, из которого компилятор создает исполняемый файл. Или единственный хедер, если проект — библиотека.
Для демонстрации взаимосвязи хедеров и `cpp`-файлов заведем проект `hello_compiler` их трех файлов:
```
hello_compiler/
├── hello_compiler.h
├── hello_compiler.cpp
└── main.cpp
```
В файле `hello_compiler.h` разместим объявление функции, которая выводит информацию о компиляторе. Она принимает строку. Класс `std::string` объявлен в хедере `string`, и нам надо подключить его через директиву препроцессора `#include`.
```c++
#include <string>
namespace sys
{
void show_compiler_info(std::string compiler);
}
```
Функция находится в пространстве имен `system`. Со временем в проект планируется добавить больше вспомогательных функций, и пространство имен подходит для их логической группировки.
Файл `hello_compiler.cpp` содержит определение `show_compiler_info()` и вспомогательную функцию `binary_name()`. Обратите внимание, что `show_compiler_info()` находится в пространстве имен `system`, как и в хедере.
```c++
#include <cstdlib>
#include <map>
#include <print>
#include "hello_compiler.h"
std::string binary_name(std::string compiler)
{
const static std::map<std::string, std::string> binaries = {
{"clang", "clang++"},
{"gcc", "g++"},
{"msvc", "cl.exe"}
};
return binaries.at(compiler);
}
namespace sys
{
void show_compiler_info(std::string compiler)
{
const std::string command = std::format("{} -v", binary_name(compiler));
const int code = std::system(command.c_str());
if (code != 0)
std::println("Couldn't get clang compiler info. Error code: {}", code);
}
}
```
В `hello_compiler.cpp` подключен наш хедер и два хедера стандартной библиотеки:
- `cstdlib`. Здесь объявлена функция [std::system()](https://en.cppreference.com/w/cpp/utility/program/system) для запуска консольной команды.
- `map`. Здесь объявлен шаблонный класс `std::map`.
- `print`. Здесь объявлена функция для вывода в консоль.
- `hello_compiler.h`. Так компилятор поймет, что мы определяем функцию, уже объявленную в другом месте.
Теперь вызовем `show_compiler_info()` из `main.cpp`. Чтобы функция стала доступна в этом файле, нужно подключить хедер с ее объявлением.
```c++
#include "hello_compiler.h"
int main()
{
sys::show_compiler_info("clang");
}
```
```
Debian clang version 20.1.7
Target: x86_64-pc-linux-gnu
Thread model: posix
...
```
### Структура проекта с хедерами и cpp-файлами
Состоящий из хедеров и `cpp`-файлов проект составляется по простым принципам:
- Объявления сущностей размещаются в хедерах, а их определения — в `cpp`-файлах.
- Если объявление вложено в [пространство имен,](/courses/cpp/chapters/cpp_chapter_0050/#block-namespaces) определение должно повторять эту вложенность.
- Чтобы использовать объявление из хедера, нужно его подключить.
- Хедеры подключаются директивой препроцессора `#include`.
- Хедеры можно подключать в `cpp`-файлы и другие хедеры.
- При подключении хедера становятся доступны все его объявления.
- Если в хедер А подключен хедер Б, и хедер А подключен в `main.cpp`, это делает содержимое хедера Б доступным в `main.cpp`. То есть подключение хедеров [транзитивно.](https://ru.wikipedia.org/wiki/%D0%A2%D1%80%D0%B0%D0%BD%D0%B7%D0%B8%D1%82%D0%B8%D0%B2%D0%BD%D0%BE%D1%81%D1%82%D1%8C)
Чтобы использовать классы и функции из стандартной библиотеки, нужно подключать соответствующие хедеры. Например, `std::vector` объявлен в хедере `vector`, функция `std::sort()` — в `algorithm`, а тип `std::size_t` — в `cstddef`. На [cppreference](https://en.cppreference.com/w/cpp/types/numeric_limits/min.html) можно узнать, что в каком хедере находится.
Хедеры подключаются с помощью директивы препроцессора `#include`. Разберемся, как это устроено.
### Директивы препроцессора
Препроцессор — это часть компилятора, отвечающая за первичную обработку кода. Он обнаруживает и обрабатывает строки в коде, начинающиеся с символа решетки `#`. За ним следует ключевое слово и опционально параметры:
```c++
#keyword params
```
Такие строки называют _директивами препроцессора._ Их обработка сводится к примитивной замене фрагментов кода. Покажем это на примере двух директив: [#include](https://en.cppreference.com/w/cpp/preprocessor/include.html) и [#define](https://en.cppreference.com/w/cpp/preprocessor/replace).
Ключевое слово `include` заменяет строку с директивой содержимым файла. Имя файла — обязательный параметр для ключевого слова `include`:
```c++
#include "common/logging.hpp"
```
Правило хорошего тона гласит: используйте директиву `#include` _только_ для подключения хедеров. Хоть технически она сработает и для произвольного файла, будь то `.cpp`, `.txt` или `.json`.
А ключевое слово `define` объявляет _макрос_ — фрагмент кода, которому дано имя.
Заведем макрос `PI` и используем его при выводе в консоль:
```c++ {.example_for_playground}
#include <print>
#define PI 3.1415926
int main()
{
std::println("{}", PI);
}
```
```
3.1415926
```
В процессе сборки проекта директива `#define` исчезнет, а по месту ее использования произойдет _макроподстановка_ — текстовая замена имени макроса на его тело:
```c++
std::println("{}", 3.1415926);
```
Макросам принято давать имена заглавными буквами, чтобы не путать их с переменными: `PI`, `MAX_JOBS` и т.д.
При макроподстановках препроцессор не проводит синтаксических или семантических (смысловых) проверок. После макроподстановок вы можете получить некорректный код и ошибку компилятора, которую трудно диагностировать и еще труднее отладить. На заре появления C++ макросы позволяли писать обобщенный код. Они стали не нужны с появлением развитых средств для создания [шаблонов](/courses/cpp/chapters/cpp_chapter_0050/#block-templates) и выполнения кода на этапе компиляции. Неудивительно, что хорошие практики современного C++ [настаивают](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Res-macros2) на отказе от макросов. А подключение хедеров через директивы препроцессоров начиная с C++20 заменяется на импорт модулей.
Директивы препроцессора — это архаичный инструмент. Но важно знать, как работать с хедерами и макросами: вы будете встречать их во всех проектах, которые не мигрировали на модули. А таких подавляющее большинство.
### Подключение хедеров
На первых строках файла принято подключать хедеры, сущности из которых используются дальше по коду. В файле `hello_compiler.cpp` проекта `hello_compiler` мы подключили хедеры:
- `hello_compiler.h`, чтобы реализовать объявленный в нем интерфейс.
- `cstdlib`, `map` и `print` из стандартной библиотеки, чтобы воспользоваться предоставляемыми ими объявленными.
```c++
#include <cstdlib>
#include <map>
#include <print>
#include "hello_compiler.h"
```
Хедеры стандартной библиотеки не имеют расширений.
Также обратите внимание, что хедеры обрамляются треугольными скобками `<>` либо кавычками `""`. От этого зависит порядок, в котором препроцессор перебирает пути для поиска файла. Стратегия перебора зависит от реализации компилятора. Она [не определяется](https://eel.is/c++draft/cpp.include) стандартом C++. Однако у распространенных компиляторов стратегии схожи.
Порядок перебора путей `#include "file"`:
1. Директория, содержащая обрабатываемый файл с директивой `#include`.
2. Другие директории проекта.
3. Набор путей по умолчанию. Их список отличается в зависимости от компилятора и дистрибутива ОС. Это так называемые [стандартные системные директории.](https://gcc.gnu.org/onlinedocs/gcc-4.5.4/cpp/Search-Path.html) Например, `/usr/local/include` и `/usr/include/`.
Перебор директорий в случае `#include <file>` ограничивается стандартными системными директориями. Если препроцессор не находит в них нужный хедер, сборка проекта [завершается с ошибкой.](https://gcc.gnu.org/onlinedocs/cpp/Search-Path.html)
Пути для поиска хедеров можно дополнить или полностью переопределить с помощью [опций компилятора и переменных окружения.](https://gcc.gnu.org/onlinedocs/gcc/Directory-Options.html#Directory-Options)
При выборе способа подключения хедера руководствуйтесь правилами:
- В треугольных скобках `<>` и без расширения указывайте хедеры стандартной библиотеки.
- В двойных кавычках `""` указывайте хедеры внутри проекта.
Перед именем хедера зачастую задается относительный путь. Усложним структуру проекта `hello_compiler`: вынесем `hello_compiler.h` и `hello_compiler.cpp` в директорию `utils`.
```
hello_compiler/
├── main.cpp
└── utils
├── hello_compiler.cpp
└── hello_compiler.h
```
Тогда подключение `hello_compiler.h` в `main.cpp` будет выглядеть так:
```c++
#include "utils/hello_compiler.h"
```
При подключении хедера технически допустимо указывать абсолютный путь, например `#include "/usr/include/boost/fusion/include/array.hpp"`. Но делать так _нельзя,_ потому что при изменении расположения хедера или при переносе проекта на другую систему сломается компиляция.
В проекте есть файл без расширения `vector`. Его имя совпадает с хедером стандартной библиотеки. Содержимое какого хедера препроцессор вставит по месту директивы `#include "vector"`? Введите букву: {.task_text}
- `s`, если хедер из стандартной библиотеки.
- `l`, если хедер внутри проекта.
```consoleoutput {.task_source #cpp_chapter_0100_task_0030}
```
Выше перечислены правила, позволяющие однозначно определить, к какому виду относится хедер. {.task_hint}
```cpp {.task_answer}
l
```
Реализуйте функцию `score_sum()`. Она принимает словарь, в котором ключ — это id абитуриента, а значение — его оценка за экзамен. Второй параметр функции — id интересующего абитуриента. Функция должна вернуть сумму баллов, которые он получил за все экзамены. Если студент не найден, функция должна вернуть 0. {.task_text}
Для суммирования используйте алгоритм [std::accumulate()](/courses/cpp/chapters/cpp_chapter_0080/#block-accumulate-overload). {.task_text}
Над функцией разместите подключение всех необходимых хедеров. Для справки используйте [cppreference](https://cppreference.com/). {.task_text}
```c++ {.task_source #cpp_chapter_0100_task_0040}
std::size_t score_sum(std::flat_multimap<std::string, std::size_t> applicants,
std::string id)
{
}
```
Вам пригодится метод [equal_range()](/courses/cpp/chapters/cpp_chapter_0070/#block-equal-range), который есть у `multi`-версий контейнеров, в том числе у класса [std::flat_multimap](/courses/cpp/chapters/cpp_chapter_0080/#block-flat). {.task_hint}
```c++ {.task_answer}
#include <flat_map>
#include <numeric>
#include <string>
#include <utility>
std::size_t fold(std::size_t left, std::pair<std::string, std::size_t> right)
{
return left + right.second;
}
std::size_t score_sum(std::flat_multimap<std::string, std::size_t> applicants,
std::string id)
{
auto[it, it_end] = applicants.equal_range(id);
return std::accumulate(it, it_end, 0, fold);
}
```
### Недостатки использования хедеров
Работа препроцессора сводится к примитивным текстовым заменам. Синтаксический и семантический анализ при этом не проводится. Вместо _каждой_ директивы `#include file` препроцессор рекурсивно подставляет содержимое файла. Рекурсивно — потому что один хедер в свою очередь может включать другие хедеры. Это приводит к **долгой сборке проекта.**
В проекте есть 3 `cpp`-файла и 2 хедера. Каждый из хедеров подключен во все `cpp`-файлы. Сколько раз в итоге препроцессор будет подставлять содержимое файла по месту директивы `#include`? {.task_text}
```consoleoutput {.task_source #cpp_chapter_0100_task_0050}
```
Каждый раз по месту директивы `#include` подставляется содержимое соответствующего заголовочного файла. Поэтому каждый из 2-х хедеров будет подставлен трижды. {.task_hint}
```cpp {.task_answer}
6
```
Содержимое одного и того же хедера может попасть в `cpp`-файл несколько раз. И не обязательно из-за ошибки. Например, если помимо хедера А подключен хедер Б, который тоже в свою очередь подключает А, прямо или косвенно. А повторное включение одного и того же кода чревато ошибками компиляции. Оно порождает **конфликтующие объявления и определения.** Чтобы этого избежать, содержимое хедера оборачивается специальной директивой препроцессора для защиты от повторного подключения (include guard). О ней мы поговорим в главе про директивы препроцессора.
Если макросы с одинаковым именем определяются в нескольких хедерах, то от _порядка включения_ хедеров зависит, тело какого макроса будет подставлено в код. Иными словами, результат подключения хедера зависит от контекста, в который он подключается. Это приводит к **нетривиальным ошибкам.**
При подключении хедера становятся доступны абсолютно все его объявления. **Отсутствуют механизмы инкапсуляции** для выбора, что скрывать, а что экспортировать.
Итак, у использования хедеров есть весомые недостатки. Поэтому в C++ был реализован принципиально другой механизм для подключения переиспользуемого кода — модули.
## Проекты с модулями
В C++20 появилась возможность создавать и подключать собственные [модули.](https://en.cppreference.com/w/cpp/language/modules.html) А в C++23 был добавлен модуль стандартной библиотеки `std`.
### Преимущества модулей
Какие задачи решают модули?
- _Инкапсуляция:_ контроль того, что экспортировать, а что скрыть как внутреннюю реализацию. При подключении хедера становятся доступны абсолютно все его объявления. А модули позволяют явно определить интерфейс, доступный при подключении модуля.
- _Предсказуемость и безопасность._ Порядок подключения хедеров имеет значение. И это приводит к нетривиальным ошибкам. Порядок подключения модулей не важен, потому что модули не экспортируют макросы.
- _Ускорение сборки._ Содержимое хедера вставляется по месту директивы `#include` всякий раз, когда компилируется файл с этой директивой. Это приводит к долгой сборке. Модуль же компилируется единожды, в результате чего создаются два файла: бинарный файл с реализацией и текстовый файл с интерфейсом.
- _Более прозрачная иерархия проекта._ Модули могут содержать подмодули. Для отражения этой вложенности в именах модулей используется точка. Например, `codecs.audio`, `codecs.video`.
- _Сокращение количества файлов._ Теперь нет необходимости держать объявления отдельно, как это было в случае с хедерами.
Разумеется, хедеры и модули прекрасно уживаются вместе. Модули можно подключать в хедеры, а хедеры — в модули. Хедеры и модули можно подключать в один и тот же файл. Но модули — это более удобная и безопасная альтернатива хедерам. Поэтому в проектах на C++20 и выше по возможности отказываются от хедеров в пользу модулей. Если вы начинаете новый проект, используйте модули и избегайте хедеры.
### Импорт модулей
Вы уже неоднократно импортировали модуль стандартной библиотеки:
```c++
import std;
```
Выражение `import std;` делает доступными экспортируемые из модуля `std` объявления.
Импортирование собственного модуля выглядит точно так же:
```c++
import codecs;
```
### Структура простого модуля
Модулям принято давать расширение `.cpp`, а интерфейсам модулей — `.cppm`. Но это всего лишь соглашение. А в некоторых проектах и интерфейс, и реализация хранятся в файлах с расширением `.cpp`.
Интерфейс модуля — это файл, который экспортирует объявления наружу. Остальные файлы модуля содержат его реализацию. Если модуль небольшой, разбивать его на несколько файлов не имеет смысла. В этой главе мы рассматриваем простой вариант организации модуля из одного файла.
В [первой практике](/courses/cpp/practice/cpp_div_without_div/) «Деление без деления» имя модуля `div` соответствует имени его файла `div.cppm`. Но имена не обязаны совпадать: мы могли бы назвать файл и `custom_math.cppm`. Так выглядит содержимое `div.cppm`:
```c++
export module div; // module declaration
import std; // import declaration
export std::size_t divide(std::size_t a, std::size_t b) // export declaration
{
// ...
}
```
Выражение `export module div;` _объявляет модуль._ Оно делает возможным импорт содержимого модуля в другие файлы. Именно эта инструкция отличает файл с интерфейсом модуля от файла с реализацией.
После объявления идет импорт модулей, сущности из которых потребуются дальше по коду. Мы подключили `std`, чтобы воспользоваться типом `std::size_t`.
Затем следует содержимое модуля. Объявления, которые должны быть доступны снаружи, помечаются ключевым словом `export`. Экспортировать можно константы, функции, классы, пространства имен и т.д.
Объявления экспортируются из модуля тремя способами.
**Экспорт каждого интересующего объявления по отдельности.** Если объявление находится внутри пространства имен, то оно тоже автоматически экспортируется наружу. Этот способ применяется, когда экспортируемых объявлений не очень много:
```c++
export void f();
namespace A
{
export void g();
}
```
**Экспорт блока `{}`,** объединяющего объявления. Этот способ удобен, если нужно экспортировать много объявлений:
```c++
export {
void f();
void g();
}
```
**Экспорт пространства имен:**
```c++
export namespace A
{
void f();
void g();
}
```
### Подключение хедеров внутри модуля
Структура модуля немного усложняется, если внутри него требуется подключить хедер. Рассмотрим это на примере модуля из [второй практики](/courses/cpp/practice/cpp_moving_average/) «Скользящее среднее»:
```c++
module; // global module fragment
#include <cmath>
export module moving_average; // module declaration
import std; // import declaration
export class MovingAverage // export declaration
{
public:
// ...
};
```
Хедер `<cmath>` был подключен, потому что в нем объявлена константа `NAN`. В модуле `std` ее нет.
Для подключения хедеров и определения макросов нужно создавать специальную секцию, которая начинается с инструкции `module;` и заканчивается инструкцией экспорта модуля. Называется эта секция [глобальным фрагментом модуля](https://en.cppreference.com/w/cpp/language/modules.html#Global_module_fragment) (global module fragment).
### Сквозной импорт модулей
Если в модуль А подключен модуль Б, и модуль А подключен в `main.cpp`, это не делает содержимое модуля Б доступным в `main.cpp`. Иными словами, импорт модулей не транзитивен.
Вернемся к модулю `div` из [первой практики.](/courses/cpp/practice/cpp_div_without_div/) В файле модуля есть импорт `std`:
```c++
export module div;
import std;
export std::size_t divide(std::size_t a, std::size_t b)
{
// ...
}
```
Модуль `div` подключается в `main.cpp`. Обратите внимание, что рядом есть импорт `std`:
```c++
import std;
import div;
int main()
{
std::size_t res = divide(11, 5);
}
```
Без этого импорта интерпретатор бы не обнаружил тип `std::size_t`:
```c++
import div;
int main()
{
std::size_t res = divide(11, 5);
}
```
```
main.cpp:5:10: error: declaration of 'size_t' must be imported from module 'std' before it is required
5 | std::size_t res = divide(11, 5);
| ^
```
То есть пользователю модуля приходится самостоятельно подключать другой модуль, в котором объявлены типы, участвующие в экспортируемых интересующим модулем объявлениях.
Можно сделать удобнее: при импорте `std` внутри `div` прокинуть этот импорт наружу. Организуется это с помощью ключевого слова `export`:
```c++
export import std;
```
### Структура проекта с модулями
Состоящий из модулей и обычных `cpp`-файлов проект строится по правилам:
- В модулях размещаются объявления и определения, которые будут переиспользованы.
- У модуля должен быть интерфейс — файл, который экспортирует модуль и его объявления.
- Модули подключаются куда угодно: в другие модули, `cpp`-файлы, хедеры.
- Если в модуль требуется подключить хедер, это делается в специальной секции, называемой глобальным фрагментом модуля.
- При подключении модуля становятся доступны только экспортируемые им объявления.
Подытожим это на примере проекта `hello_compiler`. Если переписать его с хедеров на модули, то он будет выглядеть так:
```
hello_compiler/
├── hello_compiler.cppm
└── main.cpp
```
Модуль `hello_compiler.cppm` содержит одну внутреннюю и одну экспортируемую функцию:
```c++
export module hello_compiler;
export import std;
std::string binary_name(std::string compiler)
{
const static std::map<std::string, std::string> binaries = {
{"clang", "clang++"},
{"gcc", "g++"},
{"msvc", "cl.exe"}
};
return binaries.at(compiler);
}
namespace sys
{
export void show_compiler_info(std::string compiler)
{
const std::string command = std::format("{} -v", binary_name(compiler));
const int code = std::system(command.c_str());
if (code != 0)
std::println("Couldn't get clang compiler info. Error code: {}", code);
}
}
```
Модуль подключается в `main.cpp`:
```c++
import hello_compiler;
int main()
{
sys::show_compiler_info("clang");
}
```
### Как соотносятся модули и пространства имен
У модулей и у [пространств имен](/courses/cpp/chapters/cpp_chapter_0050/#block-namespaces) (namespace) разное предназначение. Это может показаться не очевидным, если вы имели дело с модулями на Python. Усложняет ситуацию тот факт, что модуль `std` реализует одноименное пространство имен.
Так выглядит использование модуля `std`:
```c++
// Подключение экспортируемых модулем объявлений
import std;
int main()
{
// Использование класса из пространства имен std
std::vector<int> v;
}
```
А так выглядит его реализация:
```c++
export moudule std;
export namespace std
{
class vector
{
// ...
};
// ...
}
```
Пространства имен создают именованную область видимости. Они нужны, чтобы избежать конфликта имен. Одно и то же пространство имен может существовать в разных модулях. **Модули не создают новую область видимости.** Они нужны для упорядочения проекта на компоненты.
----------
## Резюме {#block-summary}
- Объявление (declaration) делает сущность видимой для компилятора, а определение (definition) реализует ее.
- Любое определение является объявлением.
- Объявлений одной и той же сущности в проекте может быть несколько. Но определение должно быть единственным. Это важный пункт ODR (one definiton rule).
- Препроцессор — компонент компилятора, который выполняет текстовые замены. Он находит и обрабатывает директивы препроцессора.
- Директива препроцессора `#inlude` подключает хедер.
- Директива препроцессора `#define` определяет макрос.
- Модули импортируются с помощью ключевого слова `import`.
- Ключевое слово `export` объявляет модуль в файле с интерфейсом модуля. Также оно необходимо для экспорта из модуля объявлений.
- Где возможно, хедерам предпочитайте модули. Это более современный способ организации кода.
Следующие главы находятся в разработке
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!