模块 (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 模块名 模块分区 (可选) attr (可选) ; |
(1) | ||||||||
export 声明 |
(2) | ||||||||
export { 声明序列 (可选) } |
(3) | ||||||||
export (可选) import 模块名 attr (可选) ; |
(4) | ||||||||
export (可选) import 模块分区 attr (可选) ; |
(5) | ||||||||
export (可选) import 头文件名称 attr (可选) ; |
(6) | ||||||||
module;
|
(7) | ||||||||
module : private;
|
(8) | ||||||||
[编辑] 模块声明
一个翻译单元可以有一个模块声明,在这种情况下它被认为是模块单元。模块声明(如果提供)必须是翻译单元的第一个声明(全局模块片段除外,稍后会介绍)。每个模块单元都与一个模块名(以及可选的分区)相关联,该名称在模块声明中提供。
export (可选) module 模块名 模块分区 (可选) 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 声明 |
|||||||||
export { 声明序列 (可选) } |
|||||||||
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 模块名 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 头文件名称 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;
预处理指令 (可选) 模块声明 |
|||||||||
如果模块单元具有全局模块片段,则其第一个声明必须是 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;
声明序列 (可选) |
|||||||||
私有模块片段结束了模块接口单元中可以影响其他翻译单元行为的部分。如果模块单元包含私有模块片段,它将是其模块的唯一模块单元。
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 模块分区 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 模块分区 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).
[编辑] 注释
特性测试 宏 | 值 | Std | 特性 |
---|---|---|---|
__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 | 模块名和模块分区可能 包含定义为类对象宏的标识符 |
已禁止 |