Non-Profit, International

Spirit unsterblich.

C++ Module

字数统计:2262

C++20的一个重要特性是模块,模块是一种全新的源文件组织方式,旨在解决以往使用源文件包含的方式导致翻译单元过大以及模板重复实例化问题,有利于加快编译速度。

模块单元

一个模块单元是一个翻译单元,这里的翻译单元不是传统意义上翻译到中间表示(IR)或者机器码(binary)的翻译单元,而是一种新的基于C++抽象机的的模块翻译单元。

一个模块单元由全局模块片段,模块声明,模块实现和私有模块片段组成,全局模块片段和私有模块片段是可选的:

 C++
// moduleLib.cpp
module; // 全局模块片段的序文
// 全局模块片段的内容
export module 模块名; // 模块接口单元声明
// module 模块名;    // 模块实现单元声明
// 模块实现
module: private;
// 私有模块片段实现

如果一个模块声明没有 export,那么它所在的文件就是一个模块实现单元。

export 声明的模块是模块接口单元,剩下的是模块实现单元。一个模块必须只有一个模块接口单元。同名的模块实现单元自动获得模块接口单元中的声明。

模块名可以带 .. 没有什么特殊含义,不过按照惯例,. 表示层次关系。

std 开头的名字不能作为模块名,这些名字是保留的。

使用 import 模块名; 可以导入模块,使用 export import 模块名; 可以使得导入当前模块的翻译单元也导入该模块的依赖模块A。

导出内容

 C++
// helloworld-impl.cpp
module helloworld;       // 模块实现单元

export void hello() {
    std::cout << "Hello world!\n";
}

export {
    int one()  { return 1; }
    int zero() { return 0; }
}

// helloworld.cpp
export module helloworld; // 声明一个模块并作为模块接口单元

export void hello();      // 声明一个导出函数

// main.cpp
import helloworld;        // 导入模块

int main() {
    hello();
}

只有在模块接口单元内进行导出,才能在该模块外被导入使用,模块接口单元决定了声明的可见性。

注意,export 应用于类,联合体和枚举时,区分声明和定义,如果在模块接口单元声明,并模块实现单元中定义,会使得它们的定义可见但不可达,见后文。

如果一个声明被模块A导出,那么它就不能被模块B定义,但是允许在模块B中进行特化(需要导入A)。

遵循以下原则:

  1. 只有模块接口单元能导出
  2. 模板全特化,模板偏特化,静态断言和C++26的consteval块不需要导出
  3. 如果存在重载,那么未导出的重载不被外部翻译单元可见,但对同模块内部可见
  4. 对于 using 声明,如果声明的是函数,导出所有关联的重载

模块和命名空间

模块和命名空间是正交的,并不是替代的关系,在模块中也可以使用命名空间,并且命名空间可以使用和所在模块相同的名字。

导出一个命名空间代表导出命名空间中的声明,同时这些声明仍然属于该命名空间。

 C++
module M;

export namespace N {}

不能导出一个匿名命名空间,因为匿名命名空间内的声明具有内部链接,同时,也不能导出一个声明为 static 的函数或者变量,这些声明也具有内部链接。

模块分区

一个模块分区是一个模块单元,模块分区必须被直接或者间接的被主模块导入:

 C++
// A-B.cpp   
export module A:B;

// A-C.cpp
module A:C;

// A.cpp
export module A; // 声明主模块单元A,并且可访问模块分区B和C

import :C;
export import :B;

模块分区单元也是模块单元,模块分区内的所有声明和定义在将其导入的模块单元中均可见,无论它们是否被导出。

模块分区可以是模块接口单元。必须被主模块接口单元第二次导出才能导出一个模块分区。

 C++
// A.cpp
export module A;     // 声明模块接口单元A
export import :Foo;  // 导入A:Foo模块分区
export int baz();    // 导出函数

// A-Foo.cpp
export module A:Foo; // 声明一个A的模块分区Foo,同时Foo也是模块接口单元
import :Internals;   // 导入A:Internals
export int foo() { return 2 * (bar() + 1); } // 导出函数

// A-Int-impl.cpp
module A:Internals;  // 声明模块分区Internals,Internals只有模块A能访问,因为没有声明模块接口 
int bar();

// A-impl.cpp

module A;
export import :Foo;  // 导入Foo分区同时导出
int bar() { return baz() - 10; }
int baz() { return 30; }

使用模块分区可以对模块进行组织和扩充,但需要导出的声明必须在模块接口单元内导出。

全局模块片段

全局模块片段的作用是将不属于本模块的定义/声明隔离。

 C++
module;  // 可选的前言,声明接下来的是全局模块片段

// 只能出现预处理指令
#define _MSC_VER 1932
#ifdef _MSC_VER
#include <win_impl.h>
#endif

export module A; // 全局模块片头只能出现在头部,并且紧跟着一个模块声明

在全局模块片段中被间接声明的声明属于全局模块而不是当前模块,全局模块片段组成全局模块。

此外,有些时候需要使用全局的预处理指令来进行控制,例如根据不同平台选择使用不同的头文件,也可以在全局模块片段中包含它们。

可见性和可达性

如果在模块翻译单元中的一个名字没有被导出,那么它对外部翻译单元就是不可见的,名字查找不能找到它。

如果一个类或者联合体或者枚举的定义在模块实现单元中并且未被导出,那么它的定义是不可达的,不能使用它的定义,并且它被视为不完整类型。

私有模块片段

私有模块片段可以定义一个非模块单元的模块实现区域,并且只能被当前模块访问,一个模块只能有一个私有模块片段。

私有模块片段通常用于在单一文件中实现整个模块,它类似于模块实现单元,但是在模块接口单元的文件中。

 C++
export module SingleFile;

// interfaces

module :private;

// implementions

私有模块片段中的声明和定义不可见也不可达。

extern "C++"

C++98的时候发明了 extern "C"extern "C++",在长时间里 extern "C++" 都没有实际作用。C++20重新利用了它。

被标记为 extern "C++" 的声明和定义作为模块实现时,属于全局模块,该功能的作用是让一个库可以被同时作为模块和头文件使用。

实践

实践中,模块接口单元起到以前的头文件的作用,模块实现单元起到源文件的作用。

对于头文件库或者可以源码分发的库,一般不需要区分模块接口单元和模块实现单元。

如果需要导出宏,那么可以将宏抽离到一个单独的头文件中,然后全局模块片段中使用 #include 导入。

需要使用传统头文件时,也在全局模块片段中使用 #include 导入。

参考
ISO/IEC 14882:2020 Programming languages — C++ Modules
最后修改:2026-05-05

若无特殊声明,本人原创文章以 CC BY-SA 4.0许可协议 提供。