C++20 Modules:从入门到真香
说实话,我一开始对 C++20 Modules 是拒绝的。编译器支持不完善、构建系统兼容性差、网上资料还少------这玩意儿能用?直到最近重构一个头文件依赖严重的项目,被 #include 的循环依赖折磨得死去活来,才决定认真研究一下 Modules。结果发现:真香。
为什么需要 Modules?
如果你写过大型 C++ 项目,一定遇到过这些问题:
编译慢。 一个头文件改了,所有依赖它的文件都要重新编译。有些项目的增量编译能跑十几分钟。
宏污染。 头文件里定义的宏,会影响到所有包含它的文件。你永远不知道某个神秘的宏是从哪来的。
循环依赖。 A.h 包含 B.h,B.h 又包含 A.h,然后就开始加前向声明、改 include 顺序,搞到心态爆炸。
Modules 就是为了解决这些问题的。
核心概念与语法
模块定义
cpp
// math.ixx (模块接口文件)
export module math;
export int add(int a, int b) {
return a + b;
}
export int multiply(int a, int b) {
return a * b;
}
关键字 export module 声明这是一个模块,export 标记对外可见的符号。
模块使用
cpp
// main.cpp
import math;
int main() {
int result = add(1, 2); // 直接使用
return 0;
}
没有 #include,没有头文件,干净利落。
模块分区
大型模块可以拆分成多个文件:
cpp
// math.ixx
export module math;
export import :core; // 导出子模块
export import :advanced;
// math-core.ixx
export module math:core;
export int add(int, int);
// math-advanced.ixx
export module math:advanced;
export int power(int, int);
实际踩坑记录
1. 编译器支持不一致
GCC、Clang、MSVC 对 Modules 的支持程度不同。我的经验:
| 编译器 | 支持情况 |
|---|---|
| MSVC | 最成熟,生产可用 |
| Clang | 基本可用,有些边界情况 |
| GCC | 支持较晚,需要 11+ 版本 |
建议:生产环境用 MSVC,或者等 GCC 12+。
2. CMake 集成
CMake 3.28+ 才有原生支持:
cmake
add_executable(myapp)
# 定义模块
add_library(math_lib)
target_sources(math_lib PUBLIC
FILE_SET CXX_MODULES FILES math.ixx
)
# 链接模块
target_link_libraries(myapp PRIVATE math_lib)
旧版 CMake 需要手写一堆命令,非常痛苦。
3. 和头文件混用
现实项目中很难一次性迁移全部代码。好消息是 Modules 和头文件可以共存:
cpp
// 混合使用
#include <vector> // 传统头文件
import my_module; // 模块
// 但注意:import 必须在所有 #include 之后
性能提升:真实数据
我用一个中等规模的项目做了测试:
| 指标 | 头文件 | Modules | 提升 |
|---|---|---|---|
| 首次编译 | 2m 30s | 1m 45s | 30% |
| 增量编译(改一个头文件) | 23s | 3s | 87% |
| 增量编译(改一个模块) | - | 2s | - |
增量编译的提速是最明显的,因为模块接口变了才需要重新编译依赖方。
迁移建议
如果你的项目满足以下条件,可以考虑迁移:
- C++20 可用 - 编译器版本足够新
-
- 头文件依赖复杂 - 有明显的编译瓶颈
-
-
愿意折腾 - 生态还不成熟,会遇到各种问题
迁移策略: -
先迁移独立的工具模块
-
- 再迁移核心库
-
- 最后迁移应用层代码
-
不要试图一次性迁移整个项目,会崩溃的。
写在最后
C++20 Modules 不是银弹,但确实解决了 C++ 长期以来的痛点。编译速度的提升是实打实的,代码组织也更清晰了。
如果你还在犹豫,我的建议是:先在 side project 里试试。踩过坑之后,再决定要不要在生产环境使用。
毕竟,新技术总得有人先吃螃蟹,为什么不是你呢?