Namespaces
Variants

Modules (since C++20)

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

Большинство проектов на C++ используют несколько единиц трансляции, поэтому им необходимо совместно использовать объявления и определения между этими единицами. Для этой цели широко используется заголовочные файлы , примером чего является стандартная библиотека , объявления которой могут быть предоставлены путем включения соответствующего заголовочного файла .

Модули — это языковая возможность для совместного использования объявлений и определений между единицами трансляции. Они являются альтернативой некоторым вариантам использования заголовочных файлов.

Модули ортогональны пространствам имён .

// helloworld.cpp
export module helloworld; // объявление модуля
import <iostream>;        // объявление импорта
export void hello()       // экспортируемое объявление
{
    std::cout << "Hello world!\n";
}
// main.cpp
import helloworld; // объявление импорта
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)
1) Объявление модуля. Объявляет, что текущая единица трансляции является модульной единицей .
2,3) Объявление экспорта. Экспортирует все объявления в пространстве имен в declaration или declaration-seq .
4,5,6) Декларация импорта. Импортирует модульную единицу/раздел модуля/заголовочную единицу.
7) Начинает global module fragment .

Объявления модулей

Единица трансляции может содержать объявление модуля, в этом случае она считается модульной единицей . Объявление модуля , если оно присутствует, должно быть первым объявлением в единице трансляции (за исключением фрагмента глобального модуля , который рассматривается далее). Каждая модульная единица связана с именем модуля (и опционально с разделом), указанным в объявлении модуля.

export (необязательно) module имя-модуля раздел-модуля  (необязательно) атрибуты  (необязательно) ;

Имя модуля состоит из одного или нескольких идентификаторов, разделённых точками (например: mymodule , mymodule.mysubmodule , mymodule2 ...). Точки не имеют внутреннего смысла, однако они неформально используются для представления иерархии.

Если любой идентификатор в имени модуля или разделе модуля определён как object-like macro , программа является некорректной.

Именованный модуль — это совокупность модульных единиц с одинаковым именем модуля.

Модульные единицы, в объявлении которых присутствует ключевое слово export , называются модульными интерфейсными единицами ; все остальные модульные единицы называются модульными реализационными единицами .

Для каждого именованного модуля должен существовать ровно один модульный интерфейсный блок, который не указывает раздел модуля; такой модульный блок называется первичным модульным интерфейсным блоком . Его экспортируемое содержимое будет доступно при импорте соответствующего именованного модуля.

// (каждая строка представляет отдельную единицу трансляции)
export module A;   // объявляет основной интерфейсный модуль для именованного модуля 'A'
module A;          // объявляет модуль реализации для именованного модуля 'A'
module A;          // объявляет еще один модуль реализации для именованного модуля 'A'
export module A.B; // объявляет основной интерфейсный модуль для именованного модуля 'A.B'
module A.B;        // объявляет модуль реализации для именованного модуля 'A.B'

Экспорт объявлений и определений

Модульные интерфейсные единицы могут экспортировать объявления (включая определения), которые могут быть импортированы другими единицами трансляции. Чтобы экспортировать объявление, либо добавьте перед ним ключевое слово export , либо поместите его внутрь блока export .

export объявление
export { последовательность-объявлений  (опционально) }
export module A; // объявляет основной модульный интерфейс для именованного модуля 'A'
// hello() будет видна модулям трансляции, импортирующим 'A'
export char const* hello() { return "hello"; } 
// world() НЕ будет видна.
char const* world() { return "world"; }
// Обе функции one() и zero() будут видимы.
export
{
    int one()  { return 1; }
    int zero() { return 0; }
}
// Экспорт пространств имён также работает: hi::english() и hi::french() будут видимы.
export namespace hi
{
    char const* english() { return "Hi!"; }
    char const* french()  { return "Salut!"; }
}

Импорт модулей и заголовочных единиц

Модули импортируются с помощью import declaration :

export (необязательно) import имя-модуля атрибуты  (необязательно) ;

Все объявления и определения, экспортируемые в модульных интерфейсных единицах данного именованного модуля, будут доступны в единице трансляции, использующей директиву импорта.

Объявления импорта могут быть экспортированы в модульном интерфейсе. То есть, если модуль B экспортирует-импортирует A , то импорт B также сделает видимыми все экспорты из A .

В модульных единицах все объявления импорта (включая экспорт-импорты) должны быть сгруппированы после объявления модуля и до всех остальных объявлений.

/////// A.cpp (основной модульный интерфейсный блок 'A')
export module A;
export char const* hello() { return "hello"; }
/////// B.cpp (основной модульный интерфейсный блок 'B')
export module B;
export import A;
export char const* world() { return "world"; }
/////// main.cpp (не модульный блок)
#include <iostream>
import B;
int main()
{
    std::cout << hello() << ' ' << world() << '\n';
}

#include не следует использовать в модульной единице (за пределами фрагмента глобального модуля ), поскольку все включенные объявления и определения будут считаться частью модуля. Вместо этого заголовки также могут быть импортированы как модули заголовков с помощью объявления импорта :

export (необязательно) import имя-заголовка атрибуты  (необязательно) ;

Хедерный модуль — это отдельная единица трансляции, синтезированная из заголовочного файла. Импорт хедерного модуля сделает доступными все его определения и объявления. Макросы препроцессора также остаются доступными (поскольку объявления импорта распознаются препроцессором).

Однако, в отличие от #include , препроцессорные макросы, уже определённые в точке объявления импорта, не будут влиять на обработку заголовка. Это может быть неудобно в некоторых случаях (некоторые заголовки используют препроцессорные макросы в качестве формы конфигурации), и в таких ситуациях требуется использование глобального фрагмента модуля .

/////// A.cpp (основной модуль интерфейса 'A')
export module A;
import <iostream>;
export import <string_view>;
export void print(std::string_view message)
{
    std::cout << message << std::endl;
}
/////// main.cpp (не модульная единица)
import A;
int main()
{
    std::string_view message = "Hello, world!";
    print(message);
}

Глобальный модульный фрагмент

Модульные единицы могут предваряться глобальным фрагментом модуля , который может использоваться для включения заголовочных файлов, когда их импортирование невозможно (в частности, когда заголовочный файл использует препроцессорные макросы в качестве конфигурации).

module;

preprocessing-directives  (optional)

module-declaration

Если модульная единица имеет глобальный фрагмент модуля, то её первое объявление должно быть module; . Затем только директивы препроцессора могут находиться в глобальном фрагменте модуля. Затем стандартное объявление модуля отмечает конец глобального фрагмента модуля и начало содержимого модуля.

/////// A.cpp (основной модуль интерфейса 'A')
module;
// Определение _POSIX_C_SOURCE добавляет функции в стандартные заголовки,
// в соответствии со стандартом POSIX.
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
export module A;
import <ctime>;
// Только для демонстрации (плохой источник случайности).
// Используйте C++ <random> вместо этого.
export double weak_random()
{
    std::timespec ts;
    std::timespec_get(&ts, TIME_UTC); // из <ctime>
    // Предоставляется в <stdlib.h> согласно стандарту POSIX.
    srand48(ts.tv_nsec);
    // drand48() возвращает случайное число между 0 и 1.
    return drand48();
}
/////// main.cpp (не модульная единица)
import <iostream>;
import A;
int main()
{
    std::cout << "Случайное значение между 0 и 1: " << weak_random() << '\n';
}

Приватный фрагмент модуля

Основной модульный интерфейс может завершаться приватным фрагментом модуля , что позволяет представить модуль в виде едичной единицы трансляции без предоставления доступа ко всему содержимому модуля импортерам.

module : private;

declaration-seq  (optional)

Приватный фрагмент модуля завершает часть модульного интерфейсного юнита, которая может влиять на поведение других единиц трансляции. Если модульный юнит содержит приватный фрагмент модуля , он будет единственным модульным юнитом своего модуля.

export module foo;
export int f();
module : private; // завершает часть модульного интерфейсного блока, которая
                  // может влиять на поведение других единиц трансляции
                  // начинает приватный фрагмент модуля
int f()           // определение недоступно для импортеров foo
{
    return 42;
}

Разделы модулей

Модуль может иметь модульные разделы . Это модульные единицы, объявления которых включают раздел модуля, начинающийся с двоеточия : и размещаемый после имени модуля.

export module A:B; // Объявляет модульный интерфейсный блок для модуля 'A', раздел ':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;     // основной модуль интерфейса
export import :B;    // Hello() видна при импорте 'A'
import :C;           // WorldImpl() теперь видна только для 'A.cpp'
// export import :C; // ОШИБКА: Нельзя экспортировать модуль реализации
// World() видна для любой единицы трансляции, импортирующей 'A'
export char const* World()
{
    return WorldImpl();
}
/////// A-B.cpp 
export module A:B; // интерфейсный модуль раздела
// Hello() видима для любой единицы трансляции, импортирующей 'A'.
export char const* Hello() { return "Hello"; }
/////// A-C.cpp 
module A:C; // модуль реализации раздела
// WorldImpl() видима для любого модульного блока 'A', импортирующего ':C'.
char const* WorldImpl() { return "World"; }
/////// main.cpp 
import A;
import <iostream>;
int main()
{
    std::cout << Hello() << ' ' << World() << '\n';
    // ОШИБКА: WorldImpl() не видна.
}

Владение модулем

В общем случае, если объявление появляется после объявления модуля в модульной единице, оно прикрепляется к этому модулю.

Если объявление сущности прикреплено к именованному модулю, эта сущность может быть определена только в этом модуле. Все объявления такой сущности должны быть прикреплены к одному и тому же модулю.

Если объявление прикреплено к именованному модулю и не экспортируется, объявленное имя имеет модульную линковку .

export module lib_A;
int f() { return 0; } // функция f имеет модульную линковку
export int x = f();   // x равно 0
export module lib_B;
int f() { return 1; } // OK, f в lib_A и f в lib_B ссылаются на разные сущности
export int y = f(); // y равно 1

Если два объявления одной сущности привязаны к разным модулям, программа является некорректной; диагностическое сообщение не требуется, если ни одно из них не достижимо из другого.

/////// decls.h
int f(); // #1, attached to the global module
int g(); // #2, attached to the global module
/////// Интерфейс модуля M
module;
#include "decls.h"
export module M;
export using ::f; // OK, не объявляет сущность, экспортирует #1
int g();          // Ошибка: соответствует #2, но привязан к M
export int h();   // #3
export int k();   // #4
/////// Другая единица трансляции
import M;
static int h();   // Ошибка: соответствует #3
int k();          // Ошибка: соответствует #4

Следующие объявления не привязаны к какому-либо именованному модулю (и, следовательно, объявленная сущность может быть определена вне модуля):

  • namespace определения с внешней линковкой;
  • объявления внутри language linkage спецификации.
export module lib_A;
namespace ns // ns не привязан к lib_A.
{
    export extern "C++" int f(); // f не привязан к lib_A.
           extern "C++" int g(); // g не привязан к lib_A.
    export              int h(); // h привязан к lib_A.
}
// ns::h должна быть определена в lib_A, но ns::f и ns::g могут быть определены в другом месте (например,
// в традиционном исходном файле).

Примечания

Макрос тестирования возможностей Значение Стандарт Функция
__cpp_modules 201907L (C++20) Модули — поддержка основного языка
__cpp_lib_modules 202207L (C++23) Модули стандартной библиотеки std и std. compat

Ключевые слова

private , module , import , export

Отчеты о дефектах

Следующие отчеты об изменениях поведения, влияющие на дефекты, были применены задним числом к ранее опубликованным стандартам C++.

DR Applied to Behavior as published Correct behavior
CWG 2732 C++20 было неясно, могут ли импортируемые заголовки
реагировать на состояние препроцессора в точке импорта
нет реакции
P3034R1 C++20 имена модулей и разделы модулей могли
содержать идентификаторы, определённые как объектоподобные макросы
запрещено