C++ Module
C++20的一个重要特性是模块,模块是一种全新的源文件组织方式,旨在解决以往使用源文件包含的方式导致翻译单元过大以及模板重复实例化问题,有利于加快编译速度。
模块单元
一个模块单元是一个翻译单元,这里的翻译单元不是传统意义上翻译到中间表示(IR)或者机器码(binary)的翻译单元,而是一种新的基于C++抽象机的的模块翻译单元。
一个模块单元由全局模块片段,模块声明,模块实现和私有模块片段组成,全局模块片段和私有模块片段是可选的:
如果一个模块声明没有 export,那么它所在的文件就是一个模块实现单元。
以 export 声明的模块是模块接口单元,剩下的是模块实现单元。一个模块必须只有一个模块接口单元。同名的模块实现单元自动获得模块接口单元中的声明。
模块名可以带 .,. 没有什么特殊含义,不过按照惯例,. 表示层次关系。
以 std 开头的名字不能作为模块名,这些名字是保留的。
使用 import 模块名; 可以导入模块,使用 export import 模块名; 可以使得导入当前模块的翻译单元也导入该模块的依赖模块A。
导出内容
只有在模块接口单元内进行导出,才能在该模块外被导入使用,模块接口单元决定了声明的可见性。
注意,export 应用于类,联合体和枚举时,区分声明和定义,如果在模块接口单元声明,并模块实现单元中定义,会使得它们的定义可见但不可达,见后文。
如果一个声明被模块A导出,那么它就不能被模块B定义,但是允许在模块B中进行特化(需要导入A)。
遵循以下原则:
- 只有模块接口单元能导出
- 模板全特化,模板偏特化,静态断言和C++26的consteval块不需要导出
- 如果存在重载,那么未导出的重载不被外部翻译单元可见,但对同模块内部可见
- 对于
using声明,如果声明的是函数,导出所有关联的重载
模块和命名空间
模块和命名空间是正交的,并不是替代的关系,在模块中也可以使用命名空间,并且命名空间可以使用和所在模块相同的名字。
导出一个命名空间代表导出命名空间中的声明,同时这些声明仍然属于该命名空间。
不能导出一个匿名命名空间,因为匿名命名空间内的声明具有内部链接,同时,也不能导出一个声明为 static 的函数或者变量,这些声明也具有内部链接。
模块分区
一个模块分区是一个模块单元,模块分区必须被直接或者间接的被主模块导入:
模块分区单元也是模块单元,模块分区内的所有声明和定义在将其导入的模块单元中均可见,无论它们是否被导出。
模块分区可以是模块接口单元。必须被主模块接口单元第二次导出才能导出一个模块分区。
使用模块分区可以对模块进行组织和扩充,但需要导出的声明必须在模块接口单元内导出。
全局模块片段
全局模块片段的作用是将不属于本模块的定义/声明隔离。
在全局模块片段中被间接声明的声明属于全局模块而不是当前模块,全局模块片段组成全局模块。
此外,有些时候需要使用全局的预处理指令来进行控制,例如根据不同平台选择使用不同的头文件,也可以在全局模块片段中包含它们。
可见性和可达性
如果在模块翻译单元中的一个名字没有被导出,那么它对外部翻译单元就是不可见的,名字查找不能找到它。
如果一个类或者联合体或者枚举的定义在模块实现单元中并且未被导出,那么它的定义是不可达的,不能使用它的定义,并且它被视为不完整类型。
私有模块片段
私有模块片段可以定义一个非模块单元的模块实现区域,并且只能被当前模块访问,一个模块只能有一个私有模块片段。
私有模块片段通常用于在单一文件中实现整个模块,它类似于模块实现单元,但是在模块接口单元的文件中。
私有模块片段中的声明和定义不可见也不可达。
extern "C++"
C++98的时候发明了 extern "C" 和 extern "C++",在长时间里 extern "C++" 都没有实际作用。C++20重新利用了它。
被标记为 extern "C++" 的声明和定义作为模块实现时,属于全局模块,该功能的作用是让一个库可以被同时作为模块和头文件使用。
实践
实践中,模块接口单元起到以前的头文件的作用,模块实现单元起到源文件的作用。
对于头文件库或者可以源码分发的库,一般不需要区分模块接口单元和模块实现单元。
如果需要导出宏,那么可以将宏抽离到一个单独的头文件中,然后全局模块片段中使用 #include 导入。
需要使用传统头文件时,也在全局模块片段中使用 #include 导入。