Модули (с C++20)
Большинство проектов на C++ используют несколько единиц трансляции, поэтому им необходимо совместно использовать объявления и определения в этих единицах. Для этого широко используются заголовки; например, в стандартной библиотеке объявления могут предоставляться посредством подключения соответствующего заголовка.
Модули — это возможность языка, позволяющая совместно использовать объявления и определения между единицами трансляции. Они являются альтернативой заголовкам в некоторых случаях.
Модули ортогональны пространствам имён.
// helloworld.cpp
export module helloworld; // module declaration
import <iostream>; // import declaration
export void hello() // export declaration
{
std::cout << "Hello world!\n";
}
// main.cpp
import helloworld; // import declaration
int main()
{
hello();
}
Синтаксис
export(необязательно) module module-name module-partition (необязательно) attr (необязательно) ; | (1) | |
export declaration | (2) | |
export { declaration-seq (необязательно) } | (3) | |
export(необязательно) import module-name attr (необязательно) ; | (4) | |
export(необязательно) import module-partition attr (необязательно) ; | (5) | |
export(необязательно) import header-name attr (необязательно) ; | (6) | |
module; | (7) | |
module : private; | (8) |
Объявления модулей
Единица трансляции может содержать объявление модуля; в таком случае она считается единицей модуля. Если объявление модуля присутствует, оно должно быть первым объявлением в единице трансляции (за исключением глобального фрагмента модуля, о котором речь пойдёт ниже). Каждая единица модуля связана с именем модуля (и, возможно, разделом), указанными в объявлении модуля.
export(необязательно) module module-name module-partition (необязательно) attr (необязательно) ; |
Имя модуля состоит из одного или нескольких идентификаторов, разделённых точками (например: mymodule, mymodule.mysubmodule, mymodule2...). Точки не имеют собственного значения, однако неформально используются для обозначения иерархии.
Если какой-либо идентификатор в имени модуля или разделе модуля определён как макрос объектного типа, программа некорректна.
Именованный модуль — это совокупность единиц модуля с одним и тем же именем модуля.
Единицы модуля, в объявлении которых присутствует ключевое слово export, называются интерфейсными единицами модуля; все остальные единицы модуля называются единицами реализации модуля.
Для каждого именованного модуля должна существовать ровно одна интерфейсная единица модуля без раздела модуля; такая единица называется основной интерфейсной единицей модуля. Её экспортируемое содержимое будет доступно при импорте соответствующего именованного модуля.
// (each line represents a separate translation unit) export module A; // declares the primary module interface unit for named module 'A' module A; // declares a module implementation unit for named module 'A' module A; // declares another module implementation unit for named module 'A' export module A.B; // declares the primary module interface unit for named module 'A.B' module A.B; // declares a module implementation unit for named module 'A.B'
Экспорт объявлений и определений
Интерфейсные единицы модуля могут экспортировать объявления (включая определения), которые можно импортировать в другие единицы трансляции. Чтобы экспортировать объявление, добавьте перед ним ключевое слово export либо поместите его в блок export.
export declaration | ||
export { declaration-seq (необязательно) } |
export module A; // declares the primary module interface unit for named module 'A'
// hello() will be visible by translations units importing 'A'
export char const* hello() { return "hello"; }
// world() will NOT be visible.
char const* world() { return "world"; }
// Both one() and zero() will be visible.
export
{
int one() { return 1; }
int zero() { return 0; }
}
// Exporting namespaces also works: hi::english() and hi::french() will be visible.
export namespace hi
{
char const* english() { return "Hi!"; }
char const* french() { return "Salut!"; }
}
Импорт модулей и единиц заголовков
Импорт модулей выполняется с помощью объявления импорта:
export(необязательно) import module-name attr (необязательно) ; |
Все объявления и определения, экспортированные в интерфейсных единицах модуля данного именованного модуля, будут доступны в единице трансляции, использующей объявление импорта.
Объявления импорта можно экспортировать в интерфейсной единице модуля. То есть, если модуль B выполняет экспортный импорт A, то при импорте B также станут видимы все экспорты из A.
В единицах модуля все объявления импорта (включая экспортные импорты) должны располагаться после объявления модуля и перед всеми остальными объявлениями.
/////// A.cpp (primary module interface unit of 'A')
export module A;
export char const* hello() { return "hello"; }
/////// B.cpp (primary module interface unit of 'B')
export module B;
export import A;
export char const* world() { return "world"; }
/////// main.cpp (not a module unit)
#include <iostream>
import B;
int main()
{
std::cout << hello() << ' ' << world() << '\n';
}
#include не следует использовать в единице модуля (за пределами глобального фрагмента модуля), поскольку все включённые объявления и определения будут считаться частью модуля. Вместо этого заголовки также можно импортировать как единицы заголовков с помощью объявления импорта:
export(необязательно) import header-name attr (необязательно) ; |
Единица заголовка — это отдельная единица трансляции, синтезированная из заголовка. Импорт единицы заголовка делает доступными все её определения и объявления. Макросы препроцессора также доступны (поскольку препроцессор распознаёт объявления импорта).
Однако, в отличие от #include, макросы препроцессора, уже определённые в точке объявления импорта, не влияют на обработку заголовка. В некоторых случаях это может быть неудобно (некоторые заголовки используют макросы препроцессора для настройки); тогда необходимо использовать глобальный фрагмент модуля.
/////// A.cpp (primary module interface unit of 'A')
export module A;
import <iostream>;
export import <string_view>;
export void print(std::string_view message)
{
std::cout << message << std::endl;
}
/////// main.cpp (not a module unit)
import A;
int main()
{
std::string_view message = "Hello, world!";
print(message);
}
Глобальный фрагмент модуля
Перед единицами модуля может располагаться глобальный фрагмент модуля, который можно использовать для подключения заголовков, когда импортировать их невозможно (в частности, когда заголовок использует макросы препроцессора для настройки).
module; preprocessing-directives (необязательно) module-declaration |
Если у единицы модуля есть глобальный фрагмент модуля, то её первым объявлением должно быть module;. Затем в глобальном фрагменте модуля могут находиться только директивы препроцессора. После этого стандартное объявление модуля отмечает конец глобального фрагмента модуля и начало содержимого модуля.
/////// A.cpp (primary module interface unit of 'A')
module;
// Defining _POSIX_C_SOURCE adds functions to standard headers,
// according to the POSIX standard.
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
export module A;
import <ctime>;
// Only for demonstration (bad source of randomness).
// Use C++ <random> instead.
export double weak_random()
{
std::timespec ts;
std::timespec_get(&ts, TIME_UTC); // from <ctime>
// Provided in <stdlib.h> according to the POSIX standard.
srand48(ts.tv_nsec);
// drand48() returns a random number between 0 and 1.
return drand48();
}
/////// main.cpp (not a module unit)
import <iostream>;
import A;
int main()
{
std::cout << "Random value between 0 and 1: " << weak_random() << '\n';
}
Приватный фрагмент модуля
В конце основной интерфейсной единицы модуля может располагаться приватный фрагмент модуля, который позволяет представить модуль в виде одной единицы трансляции, не делая всё его содержимое доступным импортирующим единицам.
module : private; declaration-seq (необязательно) |
Приватный фрагмент модуля завершает часть интерфейсной единицы модуля, которая может влиять на поведение других единиц трансляции. Если единица модуля содержит приватный фрагмент модуля, она будет единственной единицей модуля в своём модуле.
export module foo;
export int f();
module : private; // ends the portion of the module interface unit that
// can affect the behavior of other translation units
// starts a private module fragment
int f() // definition not reachable from importers of foo
{
return 42;
}
Разделы модулей
Модуль может иметь единицы-разделы модуля. Это единицы модуля, объявления модулей которых включают раздел модуля, начинающийся с двоеточия : и расположенный после имени модуля.
export module A:B; // Declares a module interface unit for module 'A', partition ':B'.
Раздел модуля соответствует ровно одной единице модуля (две единицы модуля не могут указывать на один и тот же раздел модуля). Он виден только изнутри именованного модуля (единицы трансляции за пределами именованного модуля не могут импортировать раздел модуля напрямую).
Раздел модуля могут импортировать единицы модуля того же именованного модуля.
export(необязательно) import module-partition attr (необязательно) ; |
/////// A-B.cpp export module A:B; ... /////// A-C.cpp module A:C; ... /////// A.cpp export module A; import :C; export import :B; ...
Все определения и объявления в разделе модуля видны импортирующей единице модуля независимо от того, экспортированы они или нет.
Разделы модулей могут быть интерфейсными единицами модуля (если в их объявлениях модулей присутствует export). Они должны импортироваться с экспортом основной интерфейсной единицей модуля, а их экспортированные инструкции будут видны при импорте модуля.
export(необязательно) import module-partition attr (необязательно) ; |
/////// A.cpp
export module A; // primary module interface unit
export import :B; // Hello() is visible when importing 'A'.
import :C; // WorldImpl() is now visible only for 'A.cpp'.
// export import :C; // ERROR: Cannot export a module implementation unit.
// World() is visible by any translation unit importing 'A'.
export char const* World()
{
return WorldImpl();
}
/////// A-B.cpp
export module A:B; // partition module interface unit
// Hello() is visible by any translation unit importing 'A'.
export char const* Hello() { return "Hello"; }
/////// A-C.cpp
module A:C; // partition module implementation unit
// WorldImpl() is visible by any module unit of 'A' importing ':C'.
char const* WorldImpl() { return "World"; }
/////// main.cpp
import A;
import <iostream>;
int main()
{
std::cout << Hello() << ' ' << World() << '\n';
// WorldImpl(); // ERROR: WorldImpl() is not visible.
}
Принадлежность модулю
В общем случае, если объявление находится после объявления модуля в единице модуля, оно принадлежит этому модулю.
Если объявление сущности принадлежит именованному модулю, эту сущность можно определить только в этом модуле. Все объявления такой сущности должны принадлежать одному и тому же модулю.
Если объявление принадлежит именованному модулю и не экспортируется, объявленное имя имеет связь с модулем.
export module lib_A;
int f() { return 0; } // f has module linkage
export int x = f(); // x equals 0
export module lib_B;
int f() { return 1; } // OK, f in lib_A and f in lib_B refer to different entities
export int y = f(); // y equals 1
Если два объявления сущности принадлежат разным модулям, программа некорректна; диагностика не требуется, если ни одно из объявлений недостижимо из другого.
/////// decls.h int f(); // #1, attached to the global module int g(); // #2, attached to the global module
/////// Module interface of M module; #include "decls.h" export module M; export using ::f; // OK, does not declare an entity, exports #1 int g(); // Error: matches #2, but attached to M export int h(); // #3 export int k(); // #4
/////// Other translation unit import M; static int h(); // Error: matches #3 int k(); // Error: matches #4
Следующие объявления не принадлежат ни одному именованному модулю (и, следовательно, объявленную сущность можно определить вне модуля):
- определения пространств имён с внешней связью;
- объявления внутри спецификации языковой связи.
export module lib_A;
namespace ns // ns is not attached to lib_A.
{
export extern "C++" int f(); // f is not attached to lib_A.
extern "C++" int g(); // g is not attached to lib_A.
export int h(); // h is attached to lib_A.
}
// ns::h must be defined in lib_A, but ns::f and ns::g can be defined elsewhere (e.g.
// in a traditional source file).
Примечания
| Макрос проверки поддержки возможности | Значение | Стандарт | Возможность |
|---|---|---|---|
__cpp_modules | 201907L | (C++20) | Модули — поддержка на уровне ядра языка |
__cpp_lib_modules | 202207L | (C++23) |
Модули стандартной библиотеки std и std.compat |
Ключевые слова
private, module, import, export
Отчёты об ошибках
Следующие отчёты об ошибках, изменяющие поведение, были ретроактивно применены к ранее опубликованным стандартам C++.
| DR | Применено к | Опубликованное поведение | Корректное поведение |
|---|---|---|---|
| CWG 2732 | C++20 | не было ясно, могут ли импортируемые заголовки реагировать на состояние препроцессора в точке импорта | не реагируют |
| P3034R1 | C++20 | имена модулей и разделы модулей могли содержать идентификаторы, определённые как макросы объектного типа | запрещено |
Внешние ссылки
| 1. | Chuanqi Xu, 2025 — Модули C++20: практические выводы, статус и планы (en) (zh) |
| 2. | Chuanqi Xu, 2025 — Модули C++20: лучшие практики с точки зрения пользователя (en) (zh) |
© cppreference.com
Licensed under the Creative Commons Attribution-ShareAlike Unported License v3.0.
https://en.cppreference.com/cpp/language/modules