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_awaitco_yieldco_return 三个关键字让你用同步的方式写异步代码。敬请期待!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐