现在让我们设好番茄钟放一首好听的音乐开始学习吧 🌈 😋
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):
MyModule/
├── math.ixx # 模块接口(主模块接口单元)
├── math.cpp # 模块实现文件(实现单元,可选)
└── test.cpp # 使用模块的普通源文件
标准没有强制扩展名:常见的有
.ixx、.cppm,实现文件一般仍使用.cpp。
2.2 关键语法元素
在接口单元中:
export module math; // 声明模块名称
在实现单元中:
module math; // 仅声明所属模块,不再 export
在使用方源文件中:
import math; // 使用 math 模块
与 #include <header> 不同,import 使用的是模块名,不是文件名。
模块接口文件中,凡是希望对外可见的符号都要显式 export:
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):
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开始,不能再写<strong>#include</strong>
4. 从 0 开始实现一个自定义模块示例
下面用一个 math 模块展示从声明、实现到使用的完整流程。
4.1 模块接口:math.ixx
// 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
// 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
// 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 分区的语法形式
- 分区接口:
export module time_library:core; // 分区 core 的接口
- 分区实现:
module time_library:core; // 分区 core 的实现
- 在主模块中导出分区:
export module time_library; // 主模块接口
export import :core; // 导出 core 分区
export import :formatting; // 导出 formatting 分区
- 分区之间互相导入:
export module time_library:formatting;
import :core; // 导入同一模块的另一个分区
命名规则:
模块名:分区名一起构成唯一标识- 分区名要是合法的标识符(不能以数字开头)
5.2 完整示例:时间库 time_library
目录结构:
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 # 使用示例
export module time_library;
export import :formatting; // 导出 formatting 分区
export import :core; // 导出 core 分区
export namespace bit
{
Time now_time(); // 获取当前时间
Date now_date(); // 获取当前日期
}
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;
};
}
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);
}
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};
}
}
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;,仍在完善中
示例:
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 中正常工作,一般需要:
- 确保编译器版本 ≥ 19.35
- 安装「适用于 v143 生成工具的 C++ 模块(x64/x86,实验性)」组件
- 工程属性 → C/C++ → 常规:将「扫描源以查找模块依赖关系」设为 是
- 工程属性 → 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 渐进式迁移策略
在现有项目中引入模块,建议:
- 从稳定的基础库开始模块化(如工具库、数学库等)
- 优先改写「几乎不改动」的头文件,收益最大
- 暂时保留
#include的第三方库,通过全局模块片段兼容 - 在新模块接口中只导出最小必要 API,减少耦合
8.2 常见坑位
- 模板、内联函数应放在接口单元,否则容易出现链接错误
- 不同编译器、不同版本对模块支持不完全一致:
- 需要为编译器单独配置模块扫描 / 依赖分析
- 标准库模块
import std;的实现细节仍在演进
- 构建系统层面(CMake / 自建脚本)需要适配「模块文件(如
.pcm)」的生成与依赖(现阶段使用CMake构建还存在一定的难度,跨平台死性和稳定性都还得不到确切的保证,现阶段建议在实验性项目中使用)
9. 小结
- C++20 模块提供了新一代代码组织方式,在语言层面替代了传统的「头文件 + 宏」模式
- 通过
export module/import/export等关键字,实现:- 更快的编译
- 更清晰的接口边界
- 更好的封装和可维护性
- C++23 开始推动标准库模块化,配合主流编译器,可以逐步在实际工程中实践
🚀 如果你已经在使用 C++17/20,可以从一个小型工具库开始尝试模块化,亲自感受 语义更清楚、工程更干净、编译更快速 的体验。

