coroutines и modulescoroutines and modules
import вместо #include: модуль компилируется один раз в бинарное представление, переиспользуемое при каждом импорте — без повторного парсинга текста заголовка и без утечки макросов в код, который его импортирует.import instead of #include: a module compiles once into a binary representation that's reused on every import — no re-parsing of header text, and no macro leakage into the code that imports it.
Модуль объявляется через export module name;, экспортируемые сущности (функции, классы, переменные) помечаются ключевым словом export. Использующий код пишет import name; вместо #include "name.h". Ключевое отличие от заголовков не в синтаксисе, а в модели компиляции: модуль компилируется один раз в бинарное представление (обычно файл с расширением .pcm у Clang или .ifc у MSVC — конкретное расширение и формат зависят от компилятора), которое затем переиспользуется при каждом импорте — компилятору не нужно заново парсить текст модуля в каждой единице трансляции, которая его импортирует.A module is declared with export module name;, and exported entities (functions, classes, variables) are marked with the export keyword. Code that uses it writes import name; instead of #include "name.h". The key difference from headers isn't the syntax — it's the compilation model: a module compiles once into a binary representation (typically a file with a .pcm extension for Clang, or .ifc for MSVC — the exact extension and format is compiler-specific), which is then reused on every import — the compiler doesn't need to re-parse the module's text in every translation unit that imports it.
// math_utils.h// math_utils.h
#define PI 3.14159 // просачивается всюду, где есть #include// leaks everywhere this gets #included
int square(int x);
// main.cpp — заголовок парсится заново// main.cpp — the header is re-parsed
#include "math_utils.h" // в каждой единице трансляции,// in every translation unit
// которая его включает// that includes it
// math_utils.cppm// math_utils.cppm
export module math_utils;
export int square(int x) { return x * x; }
// внутренний #define PI не виден снаружи// an internal #define PI isn't visible outside
// main.cpp — компилятор переиспользует// main.cpp — the compiler reuses
import math_utils; // уже скомпилированное бинарное представление// the already-compiled binary representation
Модель #include, унаследованная от C, накопила за десятилетия три постоянные проблемы:The #include model, inherited from C, accumulated three persistent problems over the decades:
Если 200 .cpp-файлов включают один и тот же тяжёлый заголовок, компилятор парсит его текст 200 раз — заново, с нуля, в каждом файле. Precompiled headers частично компенсируют эту стоимость, но это отдельный, менее переносимый между компиляторами и более хрупкий в настройке механизм, а не решение проблемы в её корне. На больших кодовых базах повторный парсинг заголовков составляет заметную долю времени полной пересборки.If 200 .cpp files include the same heavy header, the compiler parses its text 200 times — from scratch, in every file. Precompiled headers partially offset this cost, but they're a separate, less portable across compilers, and more fragile-to-configure mechanism — not a fix at the root of the problem. On large codebases, re-parsing headers accounts for a noticeable share of full rebuild time.
Любой #define, сделанный внутри заголовка, становится видим везде, где этот заголовок включён после — и во всех заголовках, включённых позже в том же файле. Конфликт имён макросов между независимо написанными библиотеками — источник трудноуловимых, зависящих от порядка include, ошибок сборки, которые сложно локализовать.Any #define made inside a header becomes visible everywhere that header gets included afterward — and in every header included later in the same file. Macro name collisions between independently written libraries are a source of hard-to-track, include-order-dependent build errors that are difficult to localize.
Поскольку #include — это буквально текстовая вставка, смысл заголовка может зависеть от того, что уже было определено (макросы, объявления) до его включения. Один и тот же заголовок в одном файле компилируется иначе, чем в другом, если порядок include отличается — источник багов, которые проявляются только в определённой комбинации файлов.Because #include is literally a textual insertion, a header's meaning can depend on what was already defined (macros, declarations) before it was included. The same header compiles differently in one file than in another if the include order differs — a source of bugs that only manifest under a particular combination of files.
Компилятор, встретив export module name;, компилирует модуль в самостоятельный артефакт (Binary Module Interface, BMI) — не текстовый файл, а уже разобранное, семантически проверенное представление экспортируемого интерфейса модуля. При import name; компилятор подгружает этот готовый артефакт напрямую, минуя лексический анализ и парсинг исходного текста модуля заново. Отсюда и структурная защита от утечки макросов: препроцессорные директивы внутри модуля обрабатываются на этапе его собственной компиляции и не являются частью экспортируемого интерфейса — импортирующий код просто не видит текстовое содержимое модуля, только скомпилированный результат.When the compiler encounters export module name;, it compiles the module into a standalone artifact (a Binary Module Interface, BMI) — not a text file, but an already-parsed, semantically-checked representation of the module's exported interface. On import name;, the compiler loads that ready-made artifact directly, skipping lexing and re-parsing the module's source text. This is also where the structural protection against macro leakage comes from: preprocessor directives inside a module are processed during the module's own compilation and aren't part of the exported interface — importing code simply never sees the module's textual content, only the compiled result.
// пример module-единицы с частично приватной реализацией// an example module unit with a partly private implementation
export module geometry;
export struct Point { double x, y; };
export double distance(Point a, Point b); // экспортируется// exported
namespace detail {
double sqrt_impl(double x) { /* ... *//* ... */ }
} // не помечено export — недоступно импортирующему коду вообще,// not marked export — completely inaccessible to importing code,
// не просто "приватная секция", а невидимая сущность снаружи// not just a "private section" — an entity invisible from outside
Логический модуль не обязан жить в одном файле. Module implementation unit — отдельный файл, который принадлежит тому же модулю (директива module geometry; без export), но не является интерфейсом: в нём можно писать реализацию функций, объявленных в интерфейсном файле, без риска раздуть сам интерфейс телами функций.A logical module doesn't have to live in one file. A module implementation unit is a separate file belonging to the same module (a module geometry; directive, without export) that isn't part of the interface — it's where you write the implementation of functions declared in the interface file, without bloating the interface itself with function bodies.
// geometry.cppm — интерфейс модуля, только объявления// geometry.cppm — the module interface, declarations only
export module geometry;
export double distance(Point a, Point b); // только объявление, без тела// declaration only, no body
// geometry_impl.cpp — implementation unit того же модуля// geometry_impl.cpp — an implementation unit of the same module
module geometry; // без export — это НЕ интерфейс, а часть реализации// no export — this is NOT an interface, it's part of the implementation
double distance(Point a, Point b) { // тело здесь, видно только внутри модуля geometry// the body lives here, visible only inside the geometry module
return std::hypot(a.x - b.x, a.y - b.y);
}
Ещё один способ разбить крупный модуль на файлы — module partitions: export module geometry:shapes; объявляет часть ("партицию") модуля geometry, а сам главный интерфейсный файл собирает партиции воедино через export import :shapes;. Партиции видны друг другу внутри модуля, но снаружи модуль по-прежнему выглядит как единое целое — импортирующий код пишет один import geometry;, не подозревая о внутреннем разбиении на файлы.Another way to split a large module across files is module partitions: export module geometry:shapes; declares a "partition" of the geometry module, and the main interface file pulls the partitions together via export import :shapes;. Partitions are visible to each other inside the module, but from the outside the module still looks like a single unit — importing code writes one import geometry;, unaware of the internal split across files.
Минимальный законченный сценарий: модуль с одной функцией и использующий его файл — база, на которой строятся все более сложные случаи ниже.The minimal complete scenario: a module with one function, and a file that uses it — the base on which every more elaborate case below is built.
// string_utils.cppm// string_utils.cppm
export module string_utils;
#include <string>
export std::string to_upper(std::string s) {
for (char& c : s) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
return s;
}
// main.cpp// main.cpp
import string_utils;
int main() {
std::cout << to_upper("hi"); // "HI"// "HI"
}
Фасадный модуль, объединяющий несколько нижележащих модулей в одну точку импорта для потребителя — export import делает содержимое импортированного модуля частью собственного экспортируемого интерфейса.A facade module that bundles several underlying modules into a single import point for consumers — export import makes the imported module's contents part of the facade's own exported interface.
// app_core.cppm — фасад, собирающий подсистемы воедино// app_core.cppm — a facade bundling subsystems together
export module app_core;
export import string_utils; // всё, что экспортирует string_utils, теперь видно и через app_core// everything string_utils exports is now visible through app_core too
export import geometry;
// consumer.cpp — достаточно одного импорта вместо трёх// consumer.cpp — one import is enough instead of three
import app_core; // to_upper() и distance() уже доступны// to_upper() and distance() are already available
Типичный маленький заголовок-утилита и его прямой эквивалент в виде модуля — обратите внимание, что include guard/pragma once исчезает целиком: модулю не нужна защита от повторного включения, компилятор обрабатывает его ровно один раз по своей природе.A typical small utility header and its direct module equivalent — notice that the include guard/pragma once disappears entirely: a module needs no protection against repeated inclusion, the compiler processes it exactly once by its very nature.
// до: constants.h// before: constants.h
#pragma once
constexpr double kGravity = 9.81;
inline double free_fall_distance(double t) { return 0.5 * kGravity * t * t; }
// после: constants.cppm// after: constants.cppm
export module constants;
export constexpr double kGravity = 9.81;
export double free_fall_distance(double t) { return 0.5 * kGravity * t * t; }
Нюанс: реализация шаблона должна быть видна компилятору в момент инстанциации в коде потребителя — значит, тело шаблонной функции должно находиться именно в export-части модуля (интерфейсе или партиции), а не быть спрятано в implementation unit, как в примере с distance() выше. Implementation unit подходит только для нешаблонных функций.The nuance: a template's implementation must be visible to the compiler at the point of instantiation in consumer code — which means a template function's body must live in the export part of the module (the interface, or a partition), not be tucked away in an implementation unit like distance() was above. An implementation unit only works for non-template functions.
// numeric_utils.cppm// numeric_utils.cppm
export module numeric_utils;
export template <class T>
T clamp_positive(T value) { // тело обязано быть здесь, в export-части// the body must be right here, in the export part
return value < T{} ? T{} : value;
} // вынести тело в implementation unit — компилятор не сможет// moving the body into an implementation unit — the compiler couldn't
// инстанциировать шаблон для типа T, использованного потребителем// instantiate the template for the T type used by the consumer
CMake, различия в деталях реализации между MSVC/GCC/Clang — поддержка modules после выхода C++20 была неполной и местами нестабильной довольно долго. Практическая заметка: перед тем как принимать решение мигрировать реальный проект на модули, стоит явно проверить состояние поддержки в конкретном тулчейне (версия компилятора, версия CMake, генератор сборки) — а не полагаться на то, что "раз в стандарте есть, значит работает везде одинаково".CMake, and implementation differences between MSVC/GCC/Clang — module support after C++20 shipped remained incomplete and, in places, unstable for quite a while. A practical note: before deciding to migrate a real project to modules, explicitly check the state of support in the specific toolchain (compiler version, CMake version, build generator) — don't rely on "it's in the standard, so it must work the same everywhere."
Миграция по частям — не "всё или ничего": существуют header units (заголовок, импортируемый как модуль без переписывания на export module-синтаксис) как промежуточная ступень. Но смешение добавляет реальной сложности — нужно понимать правила видимости макросов и включаемых деклараций на границе между классическим заголовком и module-единицей, и это не всегда интуитивно с первого раза.Migrating incrementally isn't "all or nothing" — header units (a header imported as a module without rewriting it into export module syntax) exist as an intermediate step. But mixing the two adds real complexity — you need to understand the visibility rules for macros and included declarations at the boundary between a classic header and a module unit, and it isn't always intuitive on the first try.
Формат BMI (.pcm/.ifc) специфичен для конкретного компилятора и обычно даже для конкретной версии этого компилятора — прекомпилированный модуль, собранный одним компилятором, как правило нельзя просто взять и использовать с другим (или даже с другой версией того же). Это усложняет распространение прекомпилированных модулей как готовых бинарных библиотек — задача, которую modules сами по себе не решают, несмотря на интуитивное ожидание "раз это бинарный формат, значит переносимый".The BMI format (.pcm/.ifc) is compiler-specific, and usually even specific to a particular version of that compiler — a precompiled module built by one compiler generally can't just be picked up and used with a different one (or even a different version of the same one). This complicates distributing precompiled modules as ready-made binary libraries — a problem modules don't solve on their own, despite the intuitive expectation that "it's a binary format, so it must be portable."