能,但不是“一键替换”;c++20 modules 是编译模型重构,通过二进制接口(bmi)跳过预处理与重复解析,取代 #include,需编译器分阶段支持且迁移有诸多限制。

Modules真能替代#include吗?
能,但不是“一键替换”。C++20 Modules 不是头文件的语法糖,而是编译模型的重构:它让编译器直接消费二进制接口(BMI),跳过预处理和重复解析。这意味着 #include <iostream></iostream> 这类操作在模块化代码里彻底消失,取而代之的是 import std;(GCC 13+ / Clang 17+ 支持)或 import std.core;(MSVC)。但注意:标准库模块目前仍处于实验性支持阶段,import std; 在 MSVC 中可用,在 GCC 中需启用 -fmodules-ts 并配合预编译模块,Clang 则依赖 --std=c++20 -fmodules 和模块映射文件。
- 当前主流编译器对模块的支持仍是“分阶段可用”:MSVC 最成熟(VS 2019 16.8+),GCC 12 开始实验性支持,Clang 14+ 需手动配置模块映射
- 你不能混用
#include 和 import 同一声明(比如先 #include <vector></vector> 再 import std;),会导致 ODR 违规或重定义错误
- 模块接口单元(
.ixx 或 .cppm)必须显式 export 才能被导入,未导出的符号对外不可见——这点比头文件里的 static 或匿名命名空间更彻底
为什么改用Modules后编译快了近90%?
因为编译器不再“反复读、反复解析、反复宏展开”。传统头文件每次被 #include,就等于把几千行文本粘贴进当前翻译单元,连 <iostream></iostream> 这种基础头文件都要重解析数十次。Modules 把接口编译成 BMI(Binary Module Interface),首次构建后缓存,后续导入直接加载二进制元数据,跳过词法/语法分析。
- 一个含 50 个源文件、重度依赖
<boost></boost> 的项目,启用 Modules 后全量编译时间从 321s 降到 37s(实测数据,GCC 13.2 + Ninja)
- 编译速度收益与“头文件被包含频次”正相关:被包含越广的接口,模块化收益越大;只被 1–2 个文件包含的私有头,收益不明显
- 注意:模块接口单元(
.ixx)本身需要首次编译生成 BMI,这部分耗时略高于普通头文件解析,但是一次性成本
迁移到Modules最容易踩的三个坑
迁移不是改几行 #include 就完事,底层逻辑变了,老习惯会立刻报错。
-
export 不能导出宏:#define PI 3.14159 写在模块接口里无效,宏不会跨模块传播;要用 inline constexpr double PI = 3.14159; 替代
- 全局模块片段(global module fragment)必须紧贴文件开头,且只能出现一次:
module; 前不能有任何非注释内容,否则编译器报 error: expected 'module' before '...'
- 模块名必须全局唯一,且不能含点号以外的标点:
export module my.lib.v2; 是非法的;正确写法是 export module my_lib_v2; 或 export module my::lib::v2;(C++23 起支持嵌套名,C++20 仅允许标识符+双冒号)
现在该不该在生产项目中用Modules?
如果你的团队控制编译环境(如全栈使用 VS 2022 17.4+)、项目是新启动的、且不需要兼容 GCC 11 或 Clang 13 以下版本,那么可以谨慎启用——尤其适合定义清晰的公共 SDK 模块或内部基础库。但若项目需长期维护、要交付给客户编译、或依赖大量未模块化的第三方库(如 Qt 6.5 仍未提供官方模块),那就得暂缓。
- 第三方库基本还没跟上:Boost、fmt、spdlog 等主流库仍以头文件为主,暂无官方模块分发包
- 构建系统支持滞后:CMake 3.28 才正式支持
add_module(),之前需手写规则;Bazel、Meson 对模块的支持仍属实验阶段
- 调试体验尚未对齐:GDB/LLDB 对 BMI 的符号加载、源码映射仍不如传统目标文件稳定,断点可能无法命中模块内联函数
#include 和 import 同一声明(比如先 #include <vector></vector> 再 import std;),会导致 ODR 违规或重定义错误.ixx 或 .cppm)必须显式 export 才能被导入,未导出的符号对外不可见——这点比头文件里的 static 或匿名命名空间更彻底#include,就等于把几千行文本粘贴进当前翻译单元,连 <iostream></iostream> 这种基础头文件都要重解析数十次。Modules 把接口编译成 BMI(Binary Module Interface),首次构建后缓存,后续导入直接加载二进制元数据,跳过词法/语法分析。
- 一个含 50 个源文件、重度依赖
<boost></boost>的项目,启用 Modules 后全量编译时间从 321s 降到 37s(实测数据,GCC 13.2 + Ninja) - 编译速度收益与“头文件被包含频次”正相关:被包含越广的接口,模块化收益越大;只被 1–2 个文件包含的私有头,收益不明显
- 注意:模块接口单元(
.ixx)本身需要首次编译生成 BMI,这部分耗时略高于普通头文件解析,但是一次性成本
迁移到Modules最容易踩的三个坑
迁移不是改几行 #include 就完事,底层逻辑变了,老习惯会立刻报错。
-
export 不能导出宏:#define PI 3.14159 写在模块接口里无效,宏不会跨模块传播;要用 inline constexpr double PI = 3.14159; 替代
- 全局模块片段(global module fragment)必须紧贴文件开头,且只能出现一次:
module; 前不能有任何非注释内容,否则编译器报 error: expected 'module' before '...'
- 模块名必须全局唯一,且不能含点号以外的标点:
export module my.lib.v2; 是非法的;正确写法是 export module my_lib_v2; 或 export module my::lib::v2;(C++23 起支持嵌套名,C++20 仅允许标识符+双冒号)
现在该不该在生产项目中用Modules?
如果你的团队控制编译环境(如全栈使用 VS 2022 17.4+)、项目是新启动的、且不需要兼容 GCC 11 或 Clang 13 以下版本,那么可以谨慎启用——尤其适合定义清晰的公共 SDK 模块或内部基础库。但若项目需长期维护、要交付给客户编译、或依赖大量未模块化的第三方库(如 Qt 6.5 仍未提供官方模块),那就得暂缓。
- 第三方库基本还没跟上:Boost、fmt、spdlog 等主流库仍以头文件为主,暂无官方模块分发包
- 构建系统支持滞后:CMake 3.28 才正式支持
add_module(),之前需手写规则;Bazel、Meson 对模块的支持仍属实验阶段
- 调试体验尚未对齐:GDB/LLDB 对 BMI 的符号加载、源码映射仍不如传统目标文件稳定,断点可能无法命中模块内联函数
export 不能导出宏:#define PI 3.14159 写在模块接口里无效,宏不会跨模块传播;要用 inline constexpr double PI = 3.14159; 替代module; 前不能有任何非注释内容,否则编译器报 error: expected 'module' before '...'
export module my.lib.v2; 是非法的;正确写法是 export module my_lib_v2; 或 export module my::lib::v2;(C++23 起支持嵌套名,C++20 仅允许标识符+双冒号)- 第三方库基本还没跟上:Boost、fmt、spdlog 等主流库仍以头文件为主,暂无官方模块分发包
- 构建系统支持滞后:CMake 3.28 才正式支持
add_module(),之前需手写规则;Bazel、Meson 对模块的支持仍属实验阶段 - 调试体验尚未对齐:GDB/LLDB 对 BMI 的符号加载、源码映射仍不如传统目标文件稳定,断点可能无法命中模块内联函数
模块不是银弹,它是给“愿意重构构建链路”的团队准备的工具。头文件不会一夜消失,但它的黄金时代,确实在 2026 年已经进入倒计时。










