Последнее обновление:2026-10-12
Модули Modules в C++20¶
Когда мы используем собственные заголовочные файлы или сторонние библиотеки, обычно применяется директива #include для подключения библиотеки — это всем привычный способ. На самом деле этот подход копирует весь код одного исходного файла (заголовочного файла) в другой файл.
Это приводит к следующим проблемам:
- Исходный файл может включаться в одну цель несколько раз, поэтому мы обычно используем
#pragma onceили#ifndef, чтобы предотвратить многократное включение исходного файла в одну единицу трансляции. - Копирование кода увеличивает время компиляции: достаточно изменить один заголовочный файл — и все файлы, косвенно включающие его, будут перекомпилированы.
- Проблемы порядка
#include: иногда возникают непонятные ошибки компиляции.
C++20 привнёс новый способ заменить директиву #include — модули.
Давайте глубоко разберёмся с модулями.
1.Модульные единицы¶
Модуль C++ состоит из одной или нескольких единиц трансляции (tu), содержащих специальные ключевые слова для объявления модуля. Такие единицы трансляции называются модульными единицами (module units).
Единицы трансляции, не являющиеся модульными, считаются частью глобального модуля; глобальный модуль анонимен, не имеет интерфейса и содержит обычный немодульный код.
1.1 Global Module Fragment¶
Модульная единица может начинаться с глобального фрагмента модуля в качестве префикса; когда невозможно импортировать заголовочный файл (в частности, когда заголовочный файл использует препроцессорные макросы в качестве конфигурации), этот глобальный фрагмент модуля может напрямую использовать прежний код.
Например: в приведённом ниже коде участок между module; и export module Foo; — это global module fragment.
module;
#include <iostream>
#ifdef Say
void hello();
#endif
export module Foo;
// purivew
void world();
Обязательно нужно помнить: если у модуля есть глобальный фрагмент модуля, первым объявлением должно быть module;, то есть помещение этого объявления в другое место приведёт к ошибке.
1.2 purview¶
purview можно понимать как всю область модуля: она начинается с объявления модуля и простирается до конца единицы трансляции.
Например: hello не входит, а world и GetData входят в purview.
module;
void hello();
// <- 这里不在Foo模块的purview内
export module Foo;
// <- 在Foo模块的purview内
void world();
export void GetData();
1.3 Private module fragment¶
Главная интерфейсная единица модуля может завершаться приватным фрагментом модуля в качестве суффикса; эта часть может появляться только в главной интерфейсной единице модуля, и если она есть, то модульная единица, в которой она появляется, должна быть единственной единицей этого модуля. Её цель — инкапсулировать интерфейс и реализацию модуля в одной единице трансляции, не раскрывая деталей реализации.
Например: я хочу создать Shape и вычислить его площадь.
Наружу достаточно expose'нуть только интерфейс создания конкретного Shape и вызывать общий интерфейс вычисления площади — так мы можем написать следующий модуль.
export module Shape;
export class Shape {
public:
virtual double CalculateArea() = 0;
};
export { std::shared_ptr<Shape> CreateRectangle(double length, double width); }
module :private; // here
class Rectangle : public Shape {
private:
double length;
double width;
public:
Rectangle(double l, double w) : length(l), width(w) {}
double CalculateArea() override { return length * width; }
};
std::shared_ptr<Shape> CreateRectangle(double length, double width) {
return std::make_shared<Rectangle>(length, width);
}
Помещать ли все внутренние детали в private? Моя версия g++ — 13, она пока этого не поддерживает и выдаст такую ошибку:
Текущую поддержку в gcc можно посмотреть здесь
Локальный clang у меня версии 16 — я проверил, и всё работает!
➜ clang++ -std=c++20 shape.cppm --precompile -o shape.pcm
➜ clang++ -std=c++20 shape.cc -fprebuilt-module-path=. shape.pcm -o shape
➜ ./shape
area is 2
Из трёх описанных частей глобальный и приватный фрагменты модуля не обязательны для существования модуля, а purview — обязательная часть модуля.
2.Использование модулей¶
2.1 Создание модуля¶
Создание модуля похоже на определение заголовочного файла: у него тоже есть свой файл, обычно с расширением .cppm. Нам достаточно использовать в этом файле ключевые слова export и module, после которых указать имя модуля, — так создаётся экспортируемый модуль. Например:
Ключевое слово export может применяться к классам, переменным и т. д.; обычно есть две формы записи:
Одна форма — ключевое слово export перед обычным объявлением; другая — блок экспорта, похожий по записи на namespace, позволяющий экспортировать сразу несколько сущностей.
2.2 Экспорт¶
Здесь возникает важный вопрос: что можно экспортировать?
- variables, classes, structs, functions, namespaces, template functions/classes, concepts — можно экспортировать
- Сущности с внутренней компоновкой не экспортируются, например static-переменные и функции, анонимные namespace.
- Объявление экспорта должно происходить на уровне пространства имён
namespace {
export void print_no_export() { // 匿名命名空间,不可导出
std::cout << "print no export" << std::endl;
}
};
namespace light {
export void print_export() { // ok
std::cout << "print export" << std::endl;
}
};
struct Foo {
export int a; // 不能导出成员变量
};
- Экспорт пространства имён неявно экспортирует всё его содержимое
export class Rectangle {
private:
double length;
double width;
public:
Rectangle(double l, double w) : length(l), width(w) {}
double CalculateArea() { return length * width; } // 隐式导出
};
export namespace {
void print_export() { // 隐式导出
std::cout << "print export" << std::endl;
}
};
export {
void func(); // 隐式导出
constexpr double PI{3.14}; // 隐式导出
};
- Первое объявление экспортируемой сущности должно быть объявлением экспорта. Последующим объявлениям и определениям ключевое слово export не требуется.
export class Foo; // ok
export class Foo; // ok,只是会冗余
class Foo { // ok,隐式export
public:
void print() { std::cout << "this is foo" << std::endl; }
};
class Bar; // not export
export class Bar; // 无效,第一个声明已经是不可导出,后续的不可导出
Foo f; // ok
f.print(); // ok
Bar b; // not ok
- Неэкспортируемый модуль не может экспортировать
module foo;
export void print { } // error: 'export' may only occur after a module interface declaration
2.3 Импорт¶
Ему в пару идёт импорт; у импорта тоже есть правила, например:
-
Нельзя импортировать самого себя
-
В модульной единице все импорты должны появляться до любых объявлений в этой модульной единице. Нельзя импортировать в произвольной точке модульной единицы.
void func() {}
import shape; // error: post-module-declaration imports must be contiguous
---------------
import shape; // ok
void func() {}
- Разрешён только импорт в глобальной области видимости
- Циклический импорт не допускается
2.4 include в модулях¶
Как использовать #include <iostream> в модуле?
2.4.1 #include в области purview¶
Замените #include на import
Например:
Компиляция с g++-13: можно с помощью c++-system-header и следующего за ним iostream скомпилировать в gcm.
g++-13 -std=c++20 -fmodules-ts -x c++-system-header iostream
g++-13 -fmodules-ts -std=c++20 -x c++ shape.cppm circle.cppm shape.cc
Если это ваш собственный заголовочный файл, например consts.h, выясняется, что его можно прямо разместить в модуле через #include, например:
2.4.2 #inlcude в области Global Module Fragment¶
В этой области можно напрямую использовать #include и макросы, например:
3.Разложение модулей¶
Когда мы хотим разбить большой модуль на более мелкие, можно воспользоваться двумя способами:
- Разделение модуля на части (partitions).
- То есть можно разложить модуль на несколько файлов. При этом для пользователя это фактически незаметно: при использовании достаточно обычного импорта модуля.
- Подмодули.
- То есть можно разложить большой модуль на иерархию из любого числа подмодулей. Пользователь может импортировать весь модуль целиком или только конкретные подмодули.
3.1 Разделение модуля на части¶
Синтаксис:
Например: у меня есть shape; для внешнего использования достаточно import shape и вызова соответствующих интерфейсов — здесь используются интерфейсы разделов circle и rectangle.
У shape есть два раздела: circle и rectangle, поэтому содержимое модуля shape.cppm таково:
Содержимое двух подразделов:
// circle.cppm
module;
#include <iostream>
export module shape:circle;
export void DrawCircle() { std::cout << "draw circle" << std::endl; }
--------------
// rectangle.cppm
module;
#include <iostream>
export module shape:rectangle;
export void DrawRectangle() { std::cout << "draw rectangle" << std::endl; }
Скомпилировав с clang и g++, я обнаружил, что clang-16 выдаёт ошибку компиляции — не поддерживает.
g++-13 поддерживает; обратите внимание, что при компиляции нужно следовать порядку подраздел -> главный раздел, иначе возникнет ошибка.
➜ g++-13 -fmodules-ts -std=c++20 -x c++ shape.cppm circle.cppm rectangle.cppm shape.cc
shape:circle: error: failed to read compiled module: No such file or directory
shape:circle: note: compiled module file is 'gcm.cache/shape-circle.gcm'
shape:circle: note: imports must be built before being imported
shape:circle: fatal error: returning to the gate for a mechanical issue
Следует писать так:
3.2 Подмодули¶
Синтаксис:
export module <module-name>.<sub_module-name>;
import <module-name>.<sub_module-name>; // 可以在import前面添加export导出该分区接口
Приведённый выше пример можно реализовать и через подмодули: оставим shape главным модулем, а двумя подмодулями будут circle и rectangle.
// shape.cppm
export module shape;
export import shape.circle;
export import shape.rectangle;
---------------------
// rectangle.cppm
module;
#include <iostream>
export module shape.rectangle;
export void DrawRectangle() { std::cout << "draw rectangle" << std::endl; }
---------------------
// circle.cppm
module;
#include <iostream>
export module shape.circle;
export void DrawCircle() { std::cout << "draw circle" << std::endl; }
Видно, что при использовании разделы модуля применяют :, а подмодули — ..
Код вызывающей стороны такой же, как для разделов модуля.
Однако при использовании между ними есть различия: например, при импорте подраздела использование его интерфейса вызывает ошибку internal compiler error: Segmentation fault: 11, тогда как подмодуль импортируется и используется нормально.
// 引入子分区
import shape:circle;
int main() {
DrawCircle(); // error
return 0;
}
// 引入子模块
import shape.circle;
int main() {
DrawCircle(); // ok
return 0;
}
Для подмодулей при import нельзя опускать имя главного модуля; выше в главном разделе мы импортировали модуль-раздел и могли использовать :circle, а здесь нельзя использовать .circle. Ошибка такова:
4.Интерфейс и реализация¶
Обычно при написании кода мы разделяем его на «заголовочный файл» и «файл реализации»; для модулей это больше не требуется, но можно по-прежнему придерживаться прежнего стиля.
Например: нарисовать shape.
- Определяем модуль shape.cppm
- Реализуем модуль shape.cc
import shape;
import <iostream>;
Shape::Shape() {}
void Shape::Draw() { std::cout << "draw a shape" << std::endl; }
Это практически аналогично привычному разделению на .h и .cpp.
https://clang.llvm.org/docs/StandardCPlusPlusModules.html
https://timsong-cpp.github.io/cppwp/n4861/module.import#7.sentence-2
Похожие темы¶
- Введение в корутины C++20 | C++那些事
- Практика с channel в C++20 | C++那些事
- Концепции Concepts в C++20 | C++那些事
- constexpr и оператор «запятая» в C++20 | C++那些事
- Порядок байтов endian в C++20 | C++那些事
- Лямбда-выражения в C++20 (часть 1) | C++那些事
- Библиотека Ranges в C++20 | C++那些事
- Дизайн и подводные камни std::span в C++20 | C++那些事
- Обзор новых возможностей C++20 | C++那些事
- [[likely]] и [[unlikely]] в C++20 | C++那些事