# Глава 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. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!