C++20 之 Modules
C++20 之 Modules
从 C 语言时代开始,
#include就是 C/C++ 世界的标准姿势——把头文件的内容原封不动地复制粘贴到源文件里。三十年过去了,这套机制的问题越来越明显:编译慢得让人抓狂、宏定义到处"泄漏"、循环依赖让人崩溃。C++20 终于带来了模块(Modules),从根源上解决了这些历史包袱——更快的编译速度、更好的封装性、更干净的依赖关系。
一、为什么需要 Modules?
在聊新语法之前,我们先看看传统头文件机制到底有什么问题。
1.1 编译速度慢
#include 的本质是文本替换——预处理器会把被包含的文件内容完整地复制到当前文件中。一个简单的 .cpp 文件经过层层包含后,实际编译的内容可能膨胀到几万甚至几十万行。
// 这一行 include 展开后可能是上万行代码
#include <iostream> // 内部又包含了 <iosfwd>, <streambuf>, <istream> ...
每次修改一个头文件中的一行代码,所有包含它的源文件都得重新编译。在大型项目中,这种"牵一发而动全身"的连锁反应是编译缓慢的主要元凶。
1.2 宏污染
头文件中的宏定义没有作用域限制。一旦某个头文件定义了宏,它会影响后续所有代码:
// Windows.h 定义了这些宏
#define min(a,b) ((a)<(b)?(a):(b))
#define max(a,b) ((a)>(b)?(a):(b))
// 然后你用 std::min 和 std::max 就会出问题 😱
int x = std::min(1, 2); // 展开后变成一坨乱码
这不只是"丑"的问题,而是会导致编译错误、逻辑 bug、难以排查的诡异行为。
1.3 循环依赖与包含守卫的无奈
头文件依赖关系形成有向图时,很容易出现循环依赖。我们不得不用 #ifndef / #pragma once 来打补丁:
#pragma once
// 或者
#ifndef MY_HEADER_H
#define MY_HEADER_H
// ... 内容 ...
#endif
这些只是防御手段,并没有从根本上解决依赖关系混乱的问题。
Modules 的核心目标: 用一种原生的、编译器能理解的模块化机制,彻底替代头文件的文本替换模式。
二、Modules 核心语法详解
2.1 一个最简单的模块
先看最直观的例子,感受一下 Modules 的用法:
模块接口文件 mathutil.ixx(.ixx 是 MSVC 的模块接口扩展名,Clang 常用 .cppm):
export module mathutil; // 定义一个名为 mathutil 的模块
export int add(int a, int b) {
return a + b;
}
export int multiply(int a, int b) {
return a * b;
}
使用模块的文件 main.cpp:
import mathutil; // 导入模块,替代 #include
#include <iostream>
int main() {
std::cout << "3 + 4 = " << add(3, 4) << std::endl;
std::cout << "3 * 4 = " << multiply(3, 4) << std::endl;
return 0;
}
对比传统头文件的方式:
| 对比项 | 头文件方式 | Modules 方式 |
|---|---|---|
| 定义 | mathutil.h + mathutil.cpp |
一个 mathutil.ixx 搞定 |
| 引用 | #include "mathutil.h" |
import mathutil; |
| 导出控制 | 没有,所有声明都可见 | export 精确控制 |
| 宏泄漏 | 可能 | 不可能 |
2.2 关键字详解
export module — 声明模块
export module 模块名;
每个模块必须有一个模块接口单元(module interface unit),它以 export module 开头,声明模块的名字。
import — 导入模块
import 模块名;
在需要使用模块功能的文件中,用 import 导入。导入后,模块中 export 的内容就可见了。
export — 控制导出
export int visible_function(); // 导出这个函数
int hidden_function(); // 不导出,模块内部使用
export class MyClass { ... }; // 导出整个类
没有 export 修饰的声明是**模块内部(module internal)**的,外部使用者无法访问。这是头文件机制做不到的封装。
2.3 模块分区(Module Partition)
当模块功能较多时,可以拆分成多个分区(partition),便于组织代码:
模块分区 mathutil-types.ixx:
export module mathutil:types; // 分区名格式:模块名:分区名(冒号分隔)
export struct Vec2 {
double x, y;
double length() const;
};
export struct Vec3 {
double x, y, z;
double length() const;
};
模块接口 mathutil.ixx:
export module mathutil;
// 导入分区并重新导出(re-export)
export import :types;
export int add(int a, int b);
export int multiply(int a, int b);
使用时只需要:
import mathutil; // 一次导入,分区的内容自动可用
int main() {
Vec2 v{3.0, 4.0};
std::cout << "length = " << v.length() << std::endl;
}
分区命名规则: 分区声明使用冒号分隔——
export module 模块名:分区标识符。例如模块foo的分区可以是export module foo:bar;、export module foo:baz;。文件命名没有标准规定,.ixx是 MSVC 的约定,Clang 常用.cppm。
2.4 模块接口文件 vs 实现文件
Modules 也支持接口与实现分离:
接口文件 greet.ixx:
export module greet;
export void sayHello(const char* name); // 只有声明,没有定义
实现文件 greet.cpp:
module; // 全局模块片段(Global Module Fragment)
#include <iostream>
module greet; // 注意:没有 export
void sayHello(const char* name) {
std::cout << "Hello, " << name << "!" << std::endl;
}
全局模块片段(Global Module Fragment):
module;和module 模块名;之间的区域用于#include传统头文件。这保证了传统 C++ 库(如<iostream>、<vector>)可以在模块中正常使用。
三、完整代码示例
示例 1:定义和使用简单模块
创建一个字符串工具模块:
strutils.ixx(模块接口):
module; // 全局模块片段开始
#include <string>
#include <algorithm>
export module strutils; // 模块接口开始
export std::string toUpper(const std::string& s) {
std::string result = s;
std::transform(result.begin(), result.end(),
result.begin(), ::toupper);
return result;
}
export std::string toLower(const std::string& s) {
std::string result = s;
std::transform(result.begin(), result.end(),
result.begin(), ::tolower);
return result;
}
export bool startsWith(const std::string& s, const std::string& prefix) {
return s.compare(0, prefix.size(), prefix) == 0;
}
main.cpp:
import strutils;
#include <iostream>
int main() {
std::string text = "Hello, C++20 Modules!";
std::cout << "Original: " << text << std::endl;
std::cout << "Upper: " << toUpper(text) << std::endl;
std::cout << "Lower: " << toLower(text) << std::endl;
std::cout << "Starts with 'Hello'? " << std::boolalpha
<< startsWith(text, "Hello") << std::endl;
return 0;
}
编译并运行:
# MSVC
cl /std:c++20 /interface strutils.ixx main.cpp
# GCC
g++ -std=c++20 -fmodules-ts strutils.ixx main.cpp -o main
# Clang
clang++ -std=c++20 --precompile strutils.ixx -o strutils.pcm
clang++ -std=c++20 main.cpp strutils.pcm -o main
输出:
Original: Hello, C++20 Modules!
Upper: HELLO, C++20 MODULES!
Lower: hello, c++20 modules!
Starts with 'Hello'? true
示例 2:导出函数、类、变量
一个完整的几何计算模块,展示多种导出方式:
geometry.ixx:
module; // 全局模块片段开始
#include <cmath>
export module geometry; // 模块接口开始
// 导出常量
export constexpr double PI = 3.14159265358979323846;
// 导出函数
export double circleArea(double radius) {
return PI * radius * radius;
}
export double circlePerimeter(double radius) {
return 2.0 * PI * radius;
}
// 导出类
export class Rectangle {
public:
Rectangle(double w, double h) : width_(w), height_(h) {}
double area() const { return width_ * height_; }
double perimeter() const { return 2.0 * (width_ + height_); }
double width() const { return width_; }
double height() const { return height_; }
private:
double width_;
double height_;
};
// 不导出的内部辅助函数(外部不可见)
double internalHelper() {
return 42.0; // 外部调用者看不到这个函数(无 export)
}
main.cpp:
import geometry;
#include <iostream>
int main() {
// 使用导出的常量
std::cout << "PI = " << PI << std::endl;
// 使用导出的函数
double r = 5.0;
std::cout << "Circle (r=" << r << "):" << std::endl;
std::cout << " Area = " << circleArea(r) << std::endl;
std::cout << " Perimeter = " << circlePerimeter(r) << std::endl;
// 使用导出的类
Rectangle rect(4.0, 3.0);
std::cout << "Rectangle (" << rect.width() << " x " << rect.height() << "):" << std::endl;
std::cout << " Area = " << rect.area() << std::endl;
std::cout << " Perimeter = " << rect.perimeter() << std::endl;
return 0;
}
注意:
internalHelper()函数虽然定义在模块中,但没有export,所以main.cpp无法调用它。这就是 Modules 提供的真正封装——未导出的实现细节对外完全不可见。
示例 3:模块分区实战
用模块分区组织一个小型"学生管理系统":
student-types.ixx(类型分区):
module;
#include <string>
#include <vector>
export module student:types;
export struct Student {
int id;
std::string name;
double gpa;
};
export using StudentList = std::vector<Student>;
student-logic.ixx(逻辑分区):
module;
#include <algorithm>
#include <numeric>
export module student:logic;
import :types;
export double averageGPA(const StudentList& students) {
if (students.empty()) return 0.0;
double sum = std::accumulate(students.begin(), students.end(), 0.0,
[](double acc, const Student& s) { return acc + s.gpa; });
return sum / students.size();
}
export Student findTopStudent(const StudentList& students) {
return *std::max_element(students.begin(), students.end(),
[](const Student& a, const Student& b) { return a.gpa < b.gpa; });
}
export StudentList filterByGPALower(const StudentList& students, double threshold) {
StudentList result;
std::copy_if(students.begin(), students.end(), std::back_inserter(result),
[threshold](const Student& s) { return s.gpa < threshold; });
return result;
}
student.ixx(主模块接口,整合所有分区):
export module student;
// 导入并重新导出所有分区
export import :types;
export import :logic;
main.cpp:
import student; // 一条 import,获得全部功能
#include <iostream>
int main() {
StudentList students = {
{1001, "Alice", 3.9},
{1002, "Bob", 3.2},
{1003, "Charlie", 2.8},
{1004, "Diana", 3.7},
{1005, "Eve", 2.5}
};
std::cout << "=== 学生信息 ===" << std::endl;
for (const auto& s : students) {
std::cout << s.id << " " << s.name << " GPA=" << s.gpa << std::endl;
}
std::cout << "\n平均 GPA: " << averageGPA(students) << std::endl;
auto top = findTopStudent(students);
std::cout << "最高 GPA: " << top.name << " (" << top.gpa << ")" << std::endl;
auto atRisk = filterByGPALower(students, 3.0);
std::cout << "\nGPA < 3.0 的学生:" << std::endl;
for (const auto& s : atRisk) {
std::cout << " - " << s.name << " (" << s.gpa << ")" << std::endl;
}
return 0;
}
这个示例展示了分区的典型用法:类型定义、业务逻辑、模块入口分层组织,外部使用者只需要一个 import student; 就能获得全部功能。
四、使用场景
4.1 替代头文件
这是最直接的使用场景。把 .h / .hpp 替换成 .ixx,用 import 替代 #include:
// 之前
#include "mylib.h"
// 之后
import mylib;
4.2 库的模块化
发布库时,可以同时提供头文件和模块两种形式:
mylib/
├── include/
│ └── mylib.h // 传统头文件,兼容 C++17 及以下
├── src/
│ ├── mylib.ixx // 模块接口文件
│ └── mylib.cpp // 模块实现文件
这样既能服务于老项目,也能让新项目享受 Modules 的好处。
4.3 加速大型项目编译
Modules 不需要重新解析头文件,编译器直接加载预编译的模块接口(类似预编译头,但更高效)。在有几百个源文件的项目中,编译加速效果非常显著。
五、注意事项
5.1 与头文件的兼容性
Modules 不是完全取代头文件的。在全局模块片段中,你仍然需要 #include 来使用传统的 C++ 标准库:
module; // 全局模块片段开始
#include <vector> // 传统头文件
#include <string>
export module mymodule; // 模块接口开始
export void func() {
std::vector<int> v{1, 2, 3}; // 可以正常使用
}
⚠️ 最佳实践: 将
#include放在全局模块片段中(module;之后、export module之前)。虽然语法上允许在export module之后使用#include,但被包含的内容会成为模块的一部分,可能引发 ODR 问题或链接冲突。
5.2 模块的可见性规则
| 声明类型 | 模块内部可见 | 模块外部可见(已 import) |
|---|---|---|
export 修饰 |
✅ | ✅ |
无 export 修饰 |
✅ | ❌ |
这意味着模块内部的辅助函数、实现细节对外完全不透明,实现了真正的信息隐藏。
5.3 编译器支持现状
Modules 是 C++20 中实现最晚、支持最不一致的特性。截至 2026 年,各编译器的支持程度:
| 编译器 | 版本要求 | 支持状态 | 备注 |
|---|---|---|---|
| MSVC | VS 2019 16.10+ | ✅ 较完善 | 目前支持最好的编译器 |
| GCC | 11.0+ | ✅ 基本可用 | 需要 -fmodules-ts 标志 |
| Clang | 16.0+ | ✅ 基本可用 | 语法与标准有少量差异 |
💡 建议: 在生产项目中使用 Modules 前,务必测试你使用的工具链(包括构建系统、IDE、静态分析工具)是否完整支持。CMake 3.28+ 开始对 Modules 有了较好的支持。
六、编译器支持
使用 Modules 需要开启 C++20 支持,并使用相应的编译标志:
# MSVC (Visual Studio 2019+)
cl /std:c++20 /interface mymodule.ixx main.cpp
# GCC 11+
g++ -std=c++20 -fmodules-ts mymodule.ixx main.cpp -o main
# Clang 16+
clang++ -std=c++20 --precompile mymodule.ixx -o mymodule.pcm
clang++ -std=c++20 main.cpp mymodule.pcm -o main
CMake 示例(3.28+):
cmake_minimum_required(VERSION 3.28)
project(MyProject LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
add_executable(main
main.cpp
mymodule.ixx # CMake 会自动识别 .ixx 文件
)
CMake 3.28+ 能自动识别
.ixx文件并正确处理模块依赖关系,是目前最推荐的构建方式。
总结
Modules 是 C++20 中改变游戏规则的特性之一,它从编译模型层面解决了困扰 C++ 社区三十年的头文件问题:
- ✅ 编译更快:不再需要反复解析头文件,模块接口只需编译一次
- ✅ 封装更好:未
export的内容对外完全不可见,真正实现信息隐藏 - ✅ 无宏泄漏:宏定义被限制在全局模块片段中,不会污染使用者
- ✅ 依赖清晰:
import比#include更精确地表达了代码依赖关系 - ⚠️ 生态还在建设中:编译器支持、工具链适配、库的模块化迁移都需要时间
如果你正在开始一个新的 C++ 项目,而且编译器版本足够新,建议尽早尝试 Modules。它会让你的项目结构更清晰,编译体验更流畅。
📌 下一篇预告: C++20 引入的另一大重磅特性——协程(Coroutines)。异步编程不再是回调地狱,
co_await、co_yield、co_return三个关键字让你用同步的方式写异步代码。敬请期待!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)