Модули (начиная с C++20)
Большинство проектов на C++ используют несколько единиц трансляции, поэтому им необходимо совместно использовать объявления и определения в этих единицах. Для этой цели характерно использование заголовков, например стандартная библиотека, чьи объявления могут быть предоставлены путём включения соответствующего заголовка.
Модули это языковая функциональность, позволяющая обмениваться объявлениями и определениями между единицами трансляции. Они являются альтернативой некоторым вариантам использования заголовочных файлов.
Модули ортогональны пространствам имён.
// helloworld.cpp export module helloworld; // объявление модуля import <iostream>; // объявление импорта export void hello() { // объявление экспорта std::cout << "Привет мир!\n"; }
// main.cpp import helloworld; // объявление импорта int main() { hello(); }
Содержание |
[править] Синтаксис
export (необязательно) module имя-модуля раздел-модуля (необязательно) атрибуты (необязательно) ;
|
(1) | ||||||||
export объявление
|
(2) | ||||||||
export { последовательность-объявлений (необязательно) }
|
(3) | ||||||||
export (необязательно) import имя-модуля атрибуты (необязательно) ;
|
(4) | ||||||||
export (необязательно) import раздел-модуля атрибуты (необязательно) ;
|
(5) | ||||||||
export (необязательно) import имя-заголовка атрибуты (необязательно) ;
|
(6) | ||||||||
module ;
|
(7) | ||||||||
module : private ;
|
(8) | ||||||||
[править] Объявления модуля
Единицы трансляции могут иметь объявление модуля, и в этом случае они считаются модульной единицей. Объявление модуля, если оно сделано, должно быть первым объявлением единицы трансляции (за исключением глобального фрагмента модуля, который рассматривается позже). Каждая модульная единица связана с именем модуля (и, возможно, с разделом), указанным в объявлении модуля.
export (необязательно) module имя-модуля раздел-модуля (необязательно) атрибуты (необязательно) ;
|
|||||||||
Имя модуля состоит из одного или нескольких идентификаторов, разделённых точками (например: mymodule
, mymodule.mysubmodule
, mymodule2
...). Точки не имеют внутреннего значения, однако они неформально используются для представления иерархии.
Именованный модуль это набор модульных единиц с одинаковым именем.
Модульные единицы, объявление которых содержит ключевое слово 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 "привет"; } // world() НЕ будет видна. char const* world() { return "мир"; } // Будут видны как one(), так и zero(). export { int one() { return 1; } int zero() { return 0; } } // Также работает экспорт пространств имён: будут видны hi::english() и hi::french(). export namespace hi { char const* english() { return "Привет!"; } char const* french() { return "Салют!"; } }
[править] Импорт модулей и заголовков
Модули импортируются через объявление импорта:
export (необязательно) import имя-модуля атрибуты (необязательно) ;
|
|||||||||
Все объявления и определения, экспортированные в единицах интерфейса модуля данного именованного модуля, будут доступны в единице трансляции с помощью объявления импорта.
Объявления импорта могут быть экспортированы в единице интерфейса модуля. То есть, если модуль A экспортирует-импортирует B, то импорт A также сделает видимыми все экспортированные единицы из B.
В модульных единицах все объявления импорта (включая экспорт-импорт) должны быть сгруппированы после объявления модуля и перед всеми другими объявлениями.
/////// A.cpp (основная единица интерфейса модуля 'A') export module A; export char const* hello() { return "привет"; } /////// B.cpp (основная единица интерфейса модуля 'B') export module B; export import A; export char const* world() { return "мир"; } /////// main.cpp (немодульная единица) #include <iostream> import B; int main() { std::cout << hello() << ' ' << world() << '\n'; }
#include не следует использовать в модульной единице (за пределами глобального фрагмента модуля), потому что все включенные объявления и определения будут считаться частью модуля. Вместо этого заголовки также могут быть импортированы как единицы заголовков объявлением импорта:
export (необязательно) import имя-заголовка атрибуты (необязательно) ;
|
|||||||||
Единица заголовка это отдельная единица трансляции, синтезированная из заголовка. Импорт единицы заголовка делает доступными все его определения и объявления. Макросы препроцессора также доступны (поскольку объявления импорта распознаются препроцессором). Однако, в отличие от #include
, макросы предварительной обработки, определённые в единице трансляции, не повлияют на обработку заголовка. В некоторых случаях это может быть неудобно (некоторые заголовки используют макросы предварительной обработки в качестве формы конфигурации), и в этом случае необходимо использование глобального фрагмента модуля.
Импорт единицы заголовка сделает доступными все его определения и объявления. Также будут доступны макросы препроцессора (поскольку объявления импорта распознаются препроцессором). Однако, в отличие от #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 = "Привет, мир!"; print(message); }
[править] Глобальный фрагмент модуля
Модульные единицы могут иметь префикс глобального фрагмента модуля, который может использоваться для включения заголовчных файлов, когда их импорт невозможен (особенно, когда заголовочный файл использует макросы предварительной обработки в качестве конфигурации).
module;
директивы-предварительной-обработки (необязательно) объявление-модуля |
|||||||||
Если модуль-единица имеет глобальный фрагмент модуля, то его первым объявлением должно быть 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 ;
последовательность-объявлений (необязательно) |
|||||||||
Частный фрагмент модуля завершает часть единицы интерфейса модуля, которая может влиять на поведение других модулей трансляции. Если модульная единица содержит частный фрагмент модуля, это будет единственная модульная единица своего модуля.
export module foo; export int f(); module :private; // заканчивает часть единицы интерфейса модуля, которая может повлиять // на поведение других единиц трансляции // начинает частный фрагмент модуля int f() { // определение недоступно из импортёров foo return 42; }
[править] Разделы модуля
Модуль может иметь единицы разделов модуля. Это единицы модуля, объявления модулей которых включают раздел модуля, который начинается с двоеточия :
и помещается после имени модуля.
export module A:B; // Объявляет единицу интерфейса модуля для модуля 'A', раздела ':B'.
Раздел модуля представляет собой ровно одну единицу модуля (две единицы модуля не могут обозначать один и тот же раздел модуля). Они видны только изнутри именованного модуля (единицы трансляции вне именованного модуля не могут напрямую импортировать раздел модуля).
Раздел модуля может быть импортирован единицами модуля с тем же именем модуля.
export (необязательно) import раздел-модуля атрибуты (необязательно) ;
|
|||||||||
/////// 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 раздел-модуля атрибуты (необязательно) ;
|
|||||||||
/////// 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 "Привет"; }
/////// A-C.cpp module A:C; // единица раздела реализации модуля // WorldImpl() видна любой единицей модуля 'A', импортирующим ':C'. char const* WorldImpl() { return "Мир"; }
/////// main.cpp import A; import <iostream>; int main() { std::cout << Hello() << ' ' << World() << '\n'; // WorldImpl(); // ОШИБКА: 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
Если два совпадающих объявления прикреплены к разным модулям, и они оба объявляют имена с внешним связыванием, программа некорректна; никакой диагностики не требуется, если ни один из них недоступен из другого. На практике есть две модели:
- В модели слабого владения модулем считается, что такие объявления объявляют одну и ту же сущность.
- В модели сильного владения модулем считается, что они объявляют разные сущности.
export module lib_A; export constexpr int f() { return 0; } // f имеет внешнее связывание
export module lib_B; export constexpr int f() { return 1; } // В модели слабого владения: несколько определений f; компоновщик может выбрать любое // В модели строгого владения: OK, f в lib_A и f в lib_B разные сущности
Следующие объявления не присоединены к какому-либо именованному модулю (и, таким образом, объявленная сущность может быть определена вне модуля):
- определения namespace с внешним связыванием;
- объявления в спецификации языкового сязывания.
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 |