C++20模块化

C++20 模块化详解

💡 C++20 模块(Modules)是 C++ 语言自诞生以来对「头文件机制」最大的一次重构,目标是:更快的编译速度、更清晰的接口边界、更强的封装性

1. 为什么需要模块?

在 C++20 之前,我们用 #include + 头文件 来组织代码:

  • 每个 .cpp 包含同样的头文件时,编译器会重复解析所有内容

  • 复杂的 #include 链条导致编译依赖非常脆弱,一点改动就会触发大量重编译

  • 宏和实现细节会污染全局命名空间,容易出现命名冲突

模块的核心目标:

  • 一次编译,多处复用:接口单独编译成二进制描述,后续 import 直接复用

  • 边界清晰:显式导出需要暴露的符号,默认实现细节对外不可见

  • 减少宏污染:模块内部宏不会泄漏到使用者

  • 显著提升编译速度:大工程中可实现 2–5 倍的整体编译提速

2. 模块的基本概念与文件结构

2.1 模块单元(Module Unit)

一个模块由若干个「模块单元」组成,常见类型:

  • 主模块接口单元(Primary Interface Unit):模块对外暴露的主入口

  • 模块实现单元(Module Implementation Unit):仅实现,不再导出接口

  • 模块分区单元(Module Partition Unit):大模块内部的逻辑拆分

  • 全局模块片段(Global Module Fragment):为兼容旧头文件和宏而存在

一个典型工程结构示例(使用自定义模块 math):

1
2
3
4
MyModule/
├── math.ixx # 模块接口(主模块接口单元)
├── math.cpp # 模块实现文件(实现单元,可选)
└── test.cpp # 使用模块的普通源文件

标准没有强制扩展名:常见的有 .ixx.cppm,实现文件一般仍使用 .cpp

2.2 关键语法元素

在接口单元中:

1
export module math;   // 声明模块名称

在实现单元中:

1
module math;          // 仅声明所属模块,不再 export

在使用方源文件中:

1
import math;          // 使用 math 模块

#include <header> 不同,import 使用的是模块名,不是文件名。

模块接口文件中,凡是希望对外可见的符号都要显式 export

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export char const* hello();          // 导出函数声明

export
{
int one(); // 批量导出
int zero();
}

export class A // 导出类
{
public:
void f1();
private:
int _a1 {1};
};

export namespace bit // 导出命名空间中的内容
{
int add(int a, int b);
}

未加 export 的实体,只能在模块内部使用,对模块外部完全不可见。

3. 全局模块片段:与传统头文件共存

C++ 生态里大量库仍然基于 #include,模块单元本身又禁止在 module / export module 之后继续写 #include。为兼顾两者,引入了全局模块片段(global module fragment)

1
2
3
4
5
6
7
8
9
module;  // 开启全局模块片段,只能写预处理指令

#include <iostream>
#include "third_party/opengl.h"
#define OLD_MACRO 42

// 到这里为止还是“传统世界”

export module MyModule; // 或 module MyModule;

规则总结:

  • module; 之前只能是空白或注释

  • module;export module / module 之间只能出现预处理指令

  • 正文模块从 export module / module 开始,不能再写 **#include**

4. 从 0 开始实现一个自定义模块示例

下面用一个 math 模块展示从声明、实现到使用的完整流程。

4.1 模块接口:math.ixx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// math.ixx —— 模块接口单元

module; // 全局模块片段
#include <iostream>

export module math; // 声明模块名称

// 导出简单函数
export char const* hello()
{
return "hello";
}

// 未导出函数:仅模块内部可见
char const* world()
{
return "world";
}

// 批量导出
export
{
int one() { return 1; }
int zero() { return 0; }
}

// 导出命名空间内容
export namespace bit
{
int add(int a, int b);
}

// 导出模板(建议直接在接口单元中定义)
export template<class T>
void TFunc(const T& x)
{
std::cout << x << std::endl;
}

// 导出类
export class A
{
public:
void f1();
private:
int _a1 {1};
};

要点:

  • 模板一般直接定义在接口单元,否则会遇到链接问题

  • 模块内部的 world() 没有 export,外部不可见

4.2 模块实现:math.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// math.cpp —— 模块实现单元

module; // 可选全局模块片段
#include <iostream>

module math; // 指明实现所属模块

namespace bit
{
int add(int a, int b)
{
return a + b;
}
}

void A::f1()
{
std::cout << world() << std::endl; // 可以访问未导出的 world()
std::cout << "void A::f1()->" << _a1 << std::endl;
}

要点:

  • 实现单元使用 module math;,而不是 export module

  • 可以访问接口单元中未导出的内部函数(如 world()

4.3 使用模块:test.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// test.cpp —— 使用模块

import math; // 导入自定义模块

#include <iostream>
#include <vector>

int main()
{
std::cout << hello() << std::endl;
// std::cout << world() << std::endl; // ❌ 无法使用,未导出

std::cout << bit::add(2, 3) << std::endl;
TFunc(1); // 模板在模块内定义,直接可用

A aa;
aa.f1();

std::vector<int> v {1, 2, 3, 4};
for (auto e : v)
{
std::cout << e << " ";
}
std::cout << std::endl;
return 0;
}

5. 模块分区(Module Partitions)

当一个模块变得很大时,可以用模块分区将其拆出多个逻辑单元,同时对外仍然以同一个模块名出现。

5.1 分区的语法形式

  • 分区接口:
1
export module time_library:core;         // 分区 core 的接口
  • 分区实现:
1
module time_library:core;               // 分区 core 的实现
  • 在主模块中导出分区:
1
2
3
4
export module time_library;             // 主模块接口

export import :core; // 导出 core 分区
export import :formatting; // 导出 formatting 分区
  • 分区之间互相导入:
1
2
export module time_library:formatting;
import :core; // 导入同一模块的另一个分区

命名规则:

  • 模块名:分区名 一起构成唯一标识

  • 分区名要是合法的标识符(不能以数字开头)

5.2 完整示例:时间库 time_library

目录结构:

1
2
3
4
5
6
7
8
MyModule/
├── time_library.ixx # 主模块接口
├── time_core.ixx # 分区 core 接口
├── time_formatting.ixx # 分区 formatting 接口
├── time_library.cpp # 主模块实现
├── time_core.cpp # 分区 core 实现
├── time_formatting.cpp # 分区 formatting 实现
└── main.cpp # 使用示例
1
2
3
4
5
6
7
8
9
10
export module time_library;

export import :formatting; // 导出 formatting 分区
export import :core; // 导出 core 分区

export namespace bit
{
Time now_time(); // 获取当前时间
Date now_date(); // 获取当前日期
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export module time_library:core;

export namespace bit
{
class Date
{
public:
Date(int y, int m, int d);
int _y;
int _m;
int _d;
};

class Time
{
public:
Time(int h, int m, int s);
int _h;
int _m;
int _s;
};
}
1
2
3
4
5
6
7
8
9
10
11
module;                     // 全局模块片段
#include <string>

export module time_library:formatting;
import :core; // 依赖 core 分区

export namespace bit
{
std::string format_time1(const Date& d, const Time& t);
std::string format_time2(const Date& d, const Time& t);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
module;                     // 全局模块片段
#define _CRT_SECURE_NO_WARNINGS 1
#include <chrono>

module time_library;

namespace bit
{
Time now_time()
{
auto now = std::chrono::system_clock::now();
auto now_time = std::chrono::system_clock::to_time_t(now);
std::tm* local_time = std::localtime(&now_time);
int hour = local_time->tm_hour;
int minute = local_time->tm_min;
int second = local_time->tm_sec;
return {hour, minute, second};
}

Date now_date()
{
auto now = std::chrono::system_clock::now();
auto now_time = std::chrono::system_clock::to_time_t(now);
std::tm* local_time = std::localtime(&now_time);
int year = 1900 + local_time->tm_year;
int month = 1 + local_time->tm_mon;
int day = local_time->tm_mday;
return {year, month, day};
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
import time_library;    // 导入整个时间库模块

#include <iostream>

int main()
{
bit::Date d = bit::now_date();
bit::Time t = bit::now_time();

std::cout << bit::format_time1(d, t) << std::endl;
std::cout << bit::format_time2(d, t) << std::endl;
return 0;
}

6. 标准库模块化:import std;

C++20 只引入了语言级模块机制,并没有标准化「标准库模块」;到了 C++23,才正式定义了:

  • import std; 标准库的主模块

  • import std.compat; C 兼容库等内容

不同编译器支持进度:

  • MSVC:对标准库模块支持最好,较新版本支持 import std; 及细分模块

  • Clang:已有较完整的 import std; 支持,但各版本覆盖度不同

  • GCC:推进较慢,GCC 15 起开始支持 import std;,仍在完善中

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import std;          // 标准库主模块
import std.compat; // C 兼容库

int main()
{
std::vector<int> numbers {5, 2, 8, 1, 9};
std::ranges::sort(numbers); // 直接使用 ranges 算法

for (auto num : numbers)
{
std::cout << num << " ";
}
std::cout << std::endl;

// C++20 格式化
std::cout << std::format("Sorted numbers: {}n", numbers);

// 使用 span 视图
std::span<int> view {numbers};
for (auto& x : view)
{
x *= 2;
}

for (auto x : view)
{
std::cout << x << " ";
}
std::cout << std::endl;

printf("hello worldn"); // 来自 std.compat
return 0;
}

6.1 在 MSVC 中启用标准库模块(示意)

要让 import std; 在 VS2022 中正常工作,一般需要:

  1. 确保编译器版本 ≥ 19.35

  2. 安装「适用于 v143 生成工具的 C++ 模块(x64/x86,实验性)」组件

  3. 工程属性 → C/C++ → 常规:将「扫描源以查找模块依赖关系」设为

  4. 工程属性 → C/C++ → 语言:C++ 语言标准选择 C++23 或 /std:c++latest

最近 Visual Studio 2026 也正是发布了,默认支持C++20标准大家也可以去试试呢

7. 性能与工程收益

7.1 编译速度

相对于传统头文件机制,模块在大中型工程中通常可以带来:

  • 小项目:20%–50% 的编译时间优化

  • 大型项目(>10 万行):整体编译时间减少 40%–70%,甚至 2–5 倍加速

原因:

  • 接口单元只编译一次,生成 .pcm 之类的二进制描述

  • 其他翻译单元 import 时直接复用,不再重复解析头文件

  • 变更实现单元时,依赖模块接口的代码不必重新编译,增量编译效果更好

7.2 代码质量

  • 更严格的封装:只有 export 的符号可见,实现细节默认隐藏

  • 更少命名冲突:宏不再跨模块泄漏,命名空间污染大幅减少

  • 接口语义更清晰:一个模块就是一个逻辑组件,外界只需关注其接口

8. 迁移与实践建议

8.1 渐进式迁移策略

在现有项目中引入模块,建议:

  1. 稳定的基础库开始模块化(如工具库、数学库等)

  2. 优先改写「几乎不改动」的头文件,收益最大

  3. 暂时保留 #include 的第三方库,通过全局模块片段兼容

  4. 在新模块接口中只导出最小必要 API,减少耦合

8.2 常见坑位

  • 模板、内联函数应放在接口单元,否则容易出现链接错误

  • 不同编译器、不同版本对模块支持不完全一致:

需要为编译器单独配置模块扫描 / 依赖分析

  • 标准库模块 import std; 的实现细节仍在演进

构建系统层面(CMake / 自建脚本)需要适配「模块文件(如 .pcm)」的生成与依赖(现阶段使用CMake构建还存在一定的难度,跨平台死性和稳定性都还得不到确切的保证,现阶段建议在实验性项目中使用)

9. 小结

  • C++20 模块提供了新一代代码组织方式,在语言层面替代了传统的「头文件 + 宏」模式

  • 通过 export module / import / export 等关键字,实现:

更快的编译

  • 更清晰的接口边界

  • 更好的封装和可维护性

C++23 开始推动标准库模块化,配合主流编译器,可以逐步在实际工程中实践

🚀 如果你已经在使用 C++17/20,可以从一个小型工具库开始尝试模块化,亲自感受 语义更清楚、工程更干净、编译更快速 的体验。