Module
What/Why:
module 是一种新的语言特性, 提供了c++中翻译单元一种组织方式, 即将源文件声明为模块, 并定义模块中符号的可见性, 文件通过引用模块来访问模块中的符号.
它的目的是解决传统 .h 头文件来分离实现与定义的一些问题
-
暴力将内容展开
-
无法很好的控制宏定义和类型定义的可见性
-
编译缓慢
引入的新关键字包括
-
module # 声明模块, 全局/私有模块段声明 (Global/Private module fragment)
-
export # 导出符号, 模块
-
import # 导入模块/头文件
首先来看一个最简单的例子
// speech.cc
// 声明当前文件为模块, 模块名为 speech
export module speech;
// 导出一个函数
export const char* get_phrase() {
return "Hello, module!";
}
// main.cc
// 引用模块
import speech;
// 引用头文件
import <iostream>;
int main() {
// 使用模块中的符号
std::cout << get_phrase() << '\n';
return 0;
}
在例子中, 我们声明了一个叫做speech的module, 并且在其中声明实现并导出了一个函数get_phrase. 然后在main函数中使用import导入模块, 之后就可以使用get_phrase函数了, 注意我们同样可以对头文件使用import.
模块声明
我们继续来看export module speech;
这句定义, 它包含3部分export, module, speech.
- module name 声明该文件属于名为name的模块, name可以是一个合法标识符, 或由.连接的合法标识符.
- name:
identifier . name |
identifier
name 也可以是一个模块分片(module partition), 这点我们后续会讲到.
- name:
- export 表示该模块是一个模块接口单元(module interface unit), 如果没有 export 则为模块实现单元(module implement unit), 模块实现单元可以有多个, 但模块接口单元只能有一个, 且一个翻译单元也只能有一个.
// (每行表示一个单独的翻译单元)
export module A; // 为具名模块 'A' 声明主模块接口单元
module A; // 为具名模块 'A' 声明一个模块实现单元
module A; // 为具名模块 'A' 声明另一个模块实现单元
export module A.B; // 为具名模块 'A.B' 声明主模块接口单元
module A.B; // 为具名模块 'A.B' 声明一个模块实现单元
注意这里的A.B模块与A模块在语义上可以解释为A.B是A的子模块,但标准并没有声明这一点, 也就是说A.B和A在编译器眼中是两个不同的模块, 且没有任何关系(这点和其他语言不同, .* import 也是非法的).
// speech.cpp
export module speech;
export import speech.english;
export import speech.spanish;
// speech_english.cpp
export module speech.english;
export const char* get_phrase_en() {
return "Hello, world!";
}
// speech_spanish.cpp
export module speech.spanish;
export const char* get_phrase_es() {
return "¡Hola Mundo!";
}
// main.cpp
import speech;
import <iostream>;
import <cstdlib>;
int main() {
if (std::rand() % 2) {
std::cout << get_phrase_en() << '\n';
} else {
std::cout << get_phrase_es() << '\n';
}
}
这是一个语义化子模块的例子, 当你import speech, 它自动帮你import speech.english和speech.spanish, 因此可以使用到两个“子模块”的定义.
导出/导入
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!"; }
}
与之相对的是导入, 只可以导入模块或头文件, 但可以将导入再标记为导出, 可以使导入当前模块的单元也使用到该导入符号(个人感觉重导出只适用于当前模块依赖的, 因为它只是将其变为可性, 而声明还在原来的模块).
/////// 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';
}
这样看起来还挺简单的, 但仔细想想还是有一些的问题, 比如导入符号, 但符号依赖的定义没有导入, 该如何判定? 可以导出的范围是什么, 原来c语言里文件内static声明的变量和函数如果能够导出是否破坏了原有的规则? 比如下面这个例子
export module A;
class B{};
// 合法, 但对外部来说 B 依然不可见
export auto hello() {
return B{};
}
这里可以看下vector-of-bool c++ module的第二篇.
我简单总结一下几点:
- export 符号必须在符号的第一次声明就 export (命名空间除外).
- 内部连接(internal linkage)的符号不能导出 (static修饰的变量和函数, 匿名命名空间).
- 除命名空间外只能在最外层声明export, 如class内部变量不能单独export.
- 同理,最外层声明export后, 内部的符号自动被export. (这点好像是强制的, 也就是一个class不能即包含导出符号也包含未导出符号, 但对命名空间来说可以声明两次, 一次导出一次不导出, 来达到这个效果.)
- import 不能出现在出模块声明外的其他定义后.
模块分区
相较于“子模块”, 模块分区()才是正确的模块拆分支持, 它将模块组织为模块和多个分区, 编译后同属于一个模块, 语法为
export module name:subname; // 定义一个模块分区并导出
<export> import name:subname; // 导入一个模块分区
继续上文将speech
拆分的例子
// speech.cpp
export module speech;
export import :english;
export import :spanish;
// speech_english.cpp
export module speech:english;
export const char* get_phrase_en() {
return "Hello, world!";
}
// speech_spanish.cpp
export module speech:spanish;
export const char* get_phrase_es() {
return "¡Hola Mundo!";
}
// main.cpp
import speech;
import <iostream>;
import <cstdlib>;
int main() {
if (std::rand() % 2) {
std::cout << get_phrase_en() << '\n';
} else {
std::cout << get_phrase_es() << '\n';
}
}
与头文件的兼容问题
前文中我们提到了import <header>;
,是合法的, 它被叫做头文件单元导入(header-unit import), 首先我们来考虑在模块中使用#include <header>
会发生什么
首先它是一个预处理器指令, 暴力的将会把头文件中的定义和声明都导入到当前模块中, 因此它与我们新的模块系统并不十分兼容, 没有办法去控制它的可见性.
但直接对模块未知(module-not-aware)的头文件使用import
也不能保证正确, 有两点问题
- 首先根据模块的定义, 当前模块的预处理指令不会影响到其他翻译单元, 即依赖宏定义的头文件无法使用宏定义修改.
- import 不能神奇的将头文件转为模块, 非importable的头文件还是有可能污染预处理器.
因此引入了全局模块段(Global Module Fragment), 它将module依赖的include部分和module的实现部分分离,达到与头文件的兼容, 它的语法如下.
module;
// stuff ... [1]
module foo;
// module purview... [2]
我们在文件开头声明module
, 再到模块定义module foo
, 中间的这段区域就是全局模块段, 可以安全的使用头文件, 和它们依赖的宏定义, 且在这个区域只能使用预处理器指令.
最后是私有模块段(Private Module Fragment), 语法如下.
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;
}
在声明module :private;
后的内容不再被导出, 可以通该方法来实现实现和声明分离.
总结
本文总结了c++20 module的一些功能和使用示例, 它的目的主要是替代传统头文件模式的一些问题, 从而更好的组织文件结构, 控制符号可见性, 进而改进跨翻译单元的共享. 这里再贴一张使用基于module的库的示例图, 通过模块接口单元暴露接口确定BMI(binary module interface), 内部也是通过BMI依赖实现.
ref:
vector-of-bool 3篇module的文章讲到了很多细节.
A (Short) Tour of C++ Modules - Daniela Engert - CppCon 2021 这篇讲到了一些实现原理,可惜没有英文字幕.
C++20 Modules purecpp的一篇文章
如何在现阶段使用module的文章. 现阶段对module支持比较好的是msvc, 我的mac机器上支持非常有限, gcc也需要更新到10以后才能使用, 因此很多特性都不能写代码实验