模块 (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 模块名 属性 (可选) ;
|
|||||||||
在给定具名模块的模块接口单元中导出的所有声明和定义,都会在使用导入声明的翻译单元中可用。
导入的声明在模块接口单元里可以再导出。也就是说,如果模块 A 导入 B 后又导出,那么导入 A 也会使所有从 B 导出的内容可见。
在模块单元里,所有导入声明(包括带导出的导入)必须集中在模块声明后以及所有其他声明前。
/////// 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;
预处理指令序列 (可选) 模块声明 |
|||||||||
如果一个模块单元有一个全局模块片段,那么它的首个声明必须是 module;。因此在全局模块片段中只能出现预处理指令。然后用一条标准的模块声明标记这个全局模块片段的结束,后面就是模块内容。
/////// A.cpp ('A' 的主模块接口单元)
module;
// 按照 POSIX 标准,定义 _POSIX_C_SOURCE 会向标准标头中添加函数。
#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; // 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(); // 错误: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,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; // OK,不声明实体,导出 #1
int g(); // 错误:与 #2 匹配,但附着于 M
export int h(); // #3
export int k(); // #4
/////// 其他翻译单元
import M;
static int h(); // 错误:与 #3 匹配
int k(); // 错误:与 #4 匹配
以下声明不附着于任何具名模块(因此声明的这些实体可以在模块外定义):
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 | 模块名和模块分区中可以包含定义为对象式宏的标识符 | 已禁止 |