Modules (since C++20)
大多数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
模块名称 模块分区
(可选)
属性
(可选)
;
|
(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 "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!"; } }
导入模块与头文件单元
模块通过 导入声明 导入:
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
header-name attr
(可选)
;
|
|||||||||
头文件单元是从头文件合成的独立翻译单元。导入头文件单元将使其所有定义和声明可被访问。预处理器宏同样可被访问(因为导入声明会被预处理器识别)。
然而,与 #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;
预处理指令 (可选) 模块声明 |
|||||||||
如果一个模块单元拥有全局模块片段,则其首个声明必须是
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> // 根据 POSIX 标准由 <stdlib.h> 提供 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; // 导入 'A' 时 Hello() 可见 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() 对任何导入 ':C' 的 'A' 模块单元可见 char const* WorldImpl() { return "World"; }
/////// 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; } // 正确:lib_A中的f与lib_B中的f指向不同实体 export int y = f(); // y等于1
如果 同一实体的两个声明 分别属于不同模块,则程序非良构;若两个声明彼此不可达,则无需提供诊断信息。
/////// decls.h int f(); // #1, 隶属于全局模块 int g(); // #2, 隶属于全局模块
/////// 模块 M 的接口 module; #include "decls.h" export module M; export using ::f; // 正确:不声明实体,导出 #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++ 标准。
| 缺陷报告 | 适用范围 | 发布时的行为 | 正确行为 |
|---|---|---|---|
| CWG 2732 | C++20 |
未明确可导入头文件是否能够
响应导入点的预处理器状态 |
不响应 |
| P3034R1 | C++20 |
模块名称和模块分区可能
包含被定义为对象式宏的标识符 |
禁止 |