# Глава 10.2. Модули
В C++20 появилась возможность создавать и подключать собственные [модули.](https://en.cppreference.com/w/cpp/language/modules.html) А в C++23 был добавлен модуль стандартной библиотеки `std`.
## Преимущества модулей
Какие задачи решают модули?
_Инкапсуляция:_ контроль того, что экспортировать, а что скрыть как внутреннюю реализацию. При подключении хедера становятся доступны абсолютно все его объявления. А модули позволяют явно определить интерфейс, доступный при подключении модуля.
_Предсказуемость и безопасность._ Порядок подключения хедеров имеет значение. И это приводит к нетривиальным ошибкам. Порядок подключения модулей не важен, потому что модули не экспортируют макросы.
_Ограничение области действия макросов._ Если макрос определён в хедере, то он становится доступен во всех файлах, которые прямо или косвенно этот хедер подключают. Модули, как уже было сказано, не экспортируют макросы.
_Ускорение сборки._ Содержимое хедера вставляется по месту директивы `#include` всякий раз, когда компилируется файл с этой директивой. Это приводит к долгой сборке. Модуль же компилируется единожды, в результате чего создаются два файла: бинарный файл с реализацией и текстовый файл с интерфейсом.
_Более прозрачная иерархия проекта._ Модули могут содержать подмодули. Для отражения этой вложенности в именах модулей используется точка. Например, `codecs.audio`, `codecs.video`.
_Сокращение количества файлов._ Теперь нет необходимости держать объявления отдельно, как это было в случае с хедерами.
Разумеется, хедеры и модули прекрасно уживаются вместе. Модули можно подключать в хедеры, а хедеры — в модули. Хедеры и модули можно подключать в один и тот же файл. Но модули — это более удобная и безопасная альтернатива хедерам. Поэтому в проектах на C++20 и выше по возможности отказываются от хедеров в пользу модулей. Если вы начинаете новый проект, используйте модули и избегайте хедеры.
## Импорт модулей
Вы уже неоднократно импортировали модуль стандартной библиотеки:
```cpp
import std;
```
Выражение `import std;` делает доступными экспортируемые из модуля `std` объявления.
Импортирование собственного модуля выглядит точно так же:
```cpp
import codecs;
```
## Структура простого модуля
Модулям принято давать расширение `.cpp`, а интерфейсам модулей — `.cppm`. Но это всего лишь соглашение. А в некоторых проектах и интерфейс, и реализация хранятся в файлах с расширением `.cpp`.
Интерфейс модуля — это файл, который экспортирует объявления наружу. Остальные файлы модуля содержат его реализацию. Если модуль небольшой, разбивать его на несколько файлов не имеет смысла. В этой главе мы рассматриваем простой вариант организации модуля из одного файла. {#block-interface}
В [первой практике](/courses/cpp/practice/cpp_div_without_div/) «Деление без деления» имя модуля `div` соответствует имени его файла `div.cppm`. Но имена не обязаны совпадать: мы могли бы назвать файл и `custom_math.cppm`. Так выглядит содержимое `div.cppm`:
```cpp
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`. Экспортировать можно константы, функции, классы, пространства имён и т.д.
Объявления экспортируются из модуля тремя способами.
**Экспорт каждого интересующего объявления по отдельности.** Если объявление находится внутри пространства имён, то оно тоже автоматически экспортируется наружу. Этот способ применяется, когда экспортируемых объявлений не очень много:
```cpp
export void f();
namespace A
{
export void g();
}
```
**Экспорт блока `{}`,** объединяющего объявления. Этот способ удобен, если нужно экспортировать много объявлений:
```cpp
export {
void f();
void g();
}
```
**Экспорт пространства имён:**
```cpp
export namespace A
{
void f();
void g();
}
```
## Подключение хедеров внутри модуля
Структура модуля немного усложняется, если внутри него требуется подключить хедер. Рассмотрим это на примере модуля из [второй практики](/courses/cpp/practice/cpp_moving_average/) «Скользящее среднее»:
```cpp
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`:
```cpp
export module div;
import std;
export std::size_t divide(std::size_t a, std::size_t b)
{
// ...
}
```
Модуль `div` подключается в `main.cpp`. Обратите внимание, что рядом есть импорт `std`:
```cpp
import std;
import div;
int main()
{
std::size_t res = divide(11, 5);
}
```
Без этого импорта интерпретатор бы не обнаружил тип `std::size_t`:
```cpp
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`:
```cpp
export import std;
```
## Структура проекта с модулями
Состоящий из модулей и обычных `cpp`-файлов проект строится по правилам:
- В модулях размещаются объявления и определения, которые будут переиспользованы.
- У модуля должен быть интерфейс — файл, который экспортирует модуль и его объявления.
- Модули подключаются куда угодно: в другие модули, `cpp`-файлы, хедеры.
- Если в модуль требуется подключить хедер, это делается в специальной секции, называемой глобальным фрагментом модуля.
- При подключении модуля становятся доступны только экспортируемые им объявления.
Подытожим это на примере проекта `hello_compiler`. Если переписать его с хедеров на модули, то он будет выглядеть так:
```
hello_compiler/
├── hello_compiler.cppm
└── main.cpp
```
Модуль `hello_compiler.cppm` содержит одну внутреннюю и одну экспортируемую функцию: {#block-project-modules}
```cpp
// hello_compiler.cppm
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`:
```cpp
// main.cpp
import hello_compiler;
int main()
{
sys::show_compiler_info("clang");
}
```
## Как соотносятся модули и пространства имён
У модулей и у [пространств имён](/courses/cpp/chapters/cpp_chapter_0053/) (namespace) разное предназначение. Это может показаться не очевидным, если вы имели дело с модулями на Python. Усложняет ситуацию тот факт, что модуль `std` реализует одноимённое пространство имён.
Так выглядит использование модуля `std`:
```cpp
// Подключение экспортируемых модулем объявлений
import std;
int main()
{
// Использование класса из пространства имён std
std::vector<int> v;
}
```
А так выглядит его реализация:
```cpp
export module std;
export namespace std
{
class vector
{
// ...
};
// ...
}
```
Пространства имён создают именованную область видимости. Они нужны, чтобы избежать конфликта имён. Одно и то же пространство имён может существовать в разных модулях. **Модули не создают новую область видимости.** Они нужны для упорядочения проекта на компоненты.
----------
## Резюме {#block-summary}
- Модули импортируются с помощью ключевого слова `import`.
- Ключевое слово `export` объявляет модуль в файле с интерфейсом модуля. Также оно необходимо для экспорта из модуля объявлений.
- Где возможно, хедерам предпочитайте модули. Это более современный способ организации кода.
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!