前置知识: C++

C++20 模块

21 min中级

C++20模块系统(Modules)完整原理:模块接口单元、分区、私有模块、编译模型、ABI影响与迁移策略

C++20 模块(Modules)

前置知识

学习目标

  • 掌握「1. 历史动机与演化」的核心机制、典型用法与常见陷阱
  • 掌握「2. 形式化定义」的核心机制、典型用法与常见陷阱
  • 掌握「3. 理论推导与证明」的核心机制、典型用法与常见陷阱
  • 掌握「4. 代码示例」的核心机制、典型用法与常见陷阱
  • 掌握「5. 对比分析」的核心机制、典型用法与常见陷阱

本章节系统讲解 C++20 引入的模块(Modules)系统,包括模块接口单元、模块实现单元、模块分区、私有模块片段、全局模块片段、头文件单元(Header Units)、模块的编译模型与依赖管理、与传统 #include 的对比、以及从大型代码库迁移到模块的工程实践。内容对标 MIT 6.170 / Stanford CS106L / CMU 15-410 课程深度,融合 GCC、Clang、MSVC 三大编译器的实现差异与 Build2、CMake Presets、Ninja 等构建系统的支持现状。


1. 历史动机与演化

1.1 头文件的根本缺陷(1985-2015)

C++ 自诞生起沿用 C 语言的头文件(Header File)机制,通过 #include 预处理指令实现代码复用。这种文本包含(Textual Inclusion)机制存在根本缺陷:

缺陷 1:编译速度慢

// 假设有 1000 个源文件,每个都包含 <iostream>
// 每次包含都会重新解析 iostream 的全部内容(约 10 万行展开后代码)
// 编译时间 = 1000 × 10 万行 = 1 亿行解析

大型项目(如 Chromium、LLVM)的编译时间可达数小时,头文件包含是主要原因。

缺陷 2:宏污染

// header_a.h
#define min(a,b) ((a)<(b)?(a):(b))

// client.cpp
#include "header_a.h"
#include <algorithm>  // std::min 被宏污染,编译失败

宏是预处理指令,作用域是文件级,包含顺序敏感,难以管理。

缺陷 3:重复包含

// 需要 include guard 或 #pragma once
#ifndef MY_HEADER_H
#define MY_HEADER_H
// ...
#endif

虽然 #pragma once 缓解了问题,但每次包含仍需解析整个文件。

缺陷 4:符号泄漏

// internal_helper.h
namespace detail { void helper(); }  // 本应私有,但被所有包含者可见

// public_api.h
#include "internal_helper.h"  // detail::helper 泄漏到所有包含 public_api.h 的文件

头文件无法真正封装内部实现。

缺陷 5:依赖脆弱

// 修改 header.h 中的任何声明(即使是非公开的私有函数)
// 会导致所有包含该头文件的源文件重新编译

修改头文件会触发大规模重编译,影响开发效率。

1.2 模块的早期探索(2012-2016)

为解决头文件的根本缺陷,C++ 社区开始探索模块化方案:

  • 2012 年:David Vandevoorde 在 C++Now 上首次提出模块提案。
  • 2014 年:Google 的 Richard Smith 提交 N4047 提案,定义模块的基本语法。
  • 2015-2016 年:Clang 实现 -fmodules-ts 作为实验性支持,Google 内部大规模使用。
// 早期 Clang 实验性语法(2015)
module math;
export int add(int a, int b);

1.3 C++20 模块的标准化(2018-2020)

2018 年,Modules 提案(P1103)在 C++ 标准化会议上通过,进入 C++20 草案:

// C++20 标准语法
export module math;

export int add(int a, int b) { return a + b; }

C++20 模块的核心设计:

  • export module 声明模块接口单元。
  • import 导入模块。
  • module :part 定义模块分区。
  • module; 声明全局模块片段。
  • module :private 声明私有模块片段。

1.4 编译器支持现状(保守表述)

截至 2025-2026 年,三大编译器的模块支持程度差异明显,总体格局是:MSVC 完整度最高, GCC 14/15 良好,Clang 仍在持续改进中(具体特性覆盖请以 cppreference 编译器支持页为准):

编译器版本支持程度BMI 格式后缀约定
MSVC19.36+(VS 2019 16.11 起成熟)完整(生产可用).ifc.ixx
GCC14/15良好(C++20 模块可用,import std 自 GCC 15 起较完整).gcm (CMI).cppm
Clang17-20持续改进中(核心可用,边角与标准库模块仍在完善).pcm (BMI).cppm

跨编译器兼容性问题:

  • BMI 格式互不兼容:GCC 的 .gcm、Clang 的 .pcm、MSVC 的 .ifc 彼此无法读取, 模块接口必须随源码一起分发并由各编译器自行重建。
  • 模块单元后缀不统一:GCC/Clang 用 .cppm,MSVC 用 .ixx。
  • 构建系统支持参差不齐:CMake 3.28+ 对 C++20 模块的扫描/依赖管理支持较完善, Ninja 原生支持依赖输出(P1689 格式),Make 需手动配置。

1.5 C++23 与 C++26 的演进

  • C++23:std::format 等标准库组件支持模块化导入(import std;);标准库模块(std、std.compat)标准化。
  • C++26(提案):P2874 提案讨论模块的 ABI 稳定性;P1689 提案改进构建系统的模块依赖发现机制;反射(P2996)与模块的深度集成。

1.6 标准库模块(C++23)

C++23 标准化了两个标准库模块:

// 标准库模块
import std;           // 包含所有标准库
import std.compat;    // 包含标准库 + C 兼容头文件

int main() {
    std::cout << "Hello, Modules!\n";  // 无需 #include <iostream>
    return 0;
}

标准库模块大幅减少编译时间,并消除宏污染(如 min/max 宏)。


2. 形式化定义

2.1 模块单元(Module Unit)

定义 3.1(模块单元):模块单元是包含模块声明的翻译单元(Translation Unit),分为三种:

ModuleUnit≜ModuleInterfaceUnit∣ModuleImplementationUnit∣ModulePartition\text{ModuleUnit} \triangleq \text{ModuleInterfaceUnit} \mid \text{ModuleImplementationUnit} \mid \text{ModulePartition}
  1. 模块接口单元(Module Interface Unit):声明 export module M;,定义模块的公开接口,可被其他翻译单元导入。
  2. 模块实现单元(Module Implementation Unit):声明 module M;(无 export),提供接口的实现,不可被导入。
  3. 模块分区(Module Partition):声明 export module M:part; 或 module M:part;,是模块内部的子单元,仅能被同模块的其他单元导入。

2.2 模块(Module)

定义 3.2(模块):模块 MM 是由模块接口单元、模块实现单元与模块分区组成的逻辑单元:

M≜⟨Interface(M),Implementation(M),Partitions(M)⟩M \triangleq \langle \text{Interface}(M), \text{Implementation}(M), \text{Partitions}(M) \rangle

其中:

  • Interface(M)\text{Interface}(M) 是唯一的模块接口单元(主接口)。
  • Implementation(M)\text{Implementation}(M) 是零个或多个模块实现单元。
  • Partitions(M)\text{Partitions}(M) 是零个或多个模块分区。

约束:每个模块只能有一个模块接口单元(主接口),但可以有多个分区(每个分区是一个独立的文件)。

2.3 导出(Export)

定义 3.3(导出声明):export 关键字声明一个符号(函数、类、变量、命名空间)为模块的公开接口:

export d≜d∈Interface(M),∀client:visible(d,client)\text{export } d \triangleq d \in \text{Interface}(M), \forall \text{client}: \text{visible}(d, \text{client})

即导出的声明对所有导入该模块的客户端可见。

可导出的声明:

  • 函数声明与定义。
  • 类声明与定义。
  • 变量声明与定义(含 constexpr)。
  • 命名空间声明。
  • 模板声明与定义。
  • 类型别名(using)。
  • 枚举与枚举类。

不可导出的声明:

  • 匿名命名空间内的声明。
  • 具有内部链接的声明(static)。
  • friend 声明(隐式)。

2.4 导入(Import)

定义 3.4(导入声明):import M; 将模块 MM 的导出声明引入当前翻译单元:

import M≜∀d∈Interface(M):visible(d,current_TU)\text{import } M \triangleq \forall d \in \text{Interface}(M): \text{visible}(d, \text{current\_TU})

性质:

  • 导入是”幂等”的:多次导入同一模块等价于一次导入。
  • 导入不传递:导入 AA 不会自动导入 AA 依赖的 BB(除非 AA 使用 export import B)。
  • 导入顺序无关:模块导入顺序不影响语义(与 #include 不同)。

2.5 模块链接(Module Linkage)

定义 3.5(模块链接):模块内非导出的声明具有模块链接(Module Linkage),仅在同模块内可见:

∀d∈Module(M),d∉exported:linkage(d)=module_linkage\forall d \in \text{Module}(M), d \notin \text{exported}: \text{linkage}(d) = \text{module\_linkage}

即模块内私有声明不会泄漏到模块外部,实现了真正的封装。

对比:

  • 外部链接(External Linkage):全局变量、非静态函数,跨翻译单元可见。
  • 内部链接(Internal Linkage):static 变量、匿名命名空间,仅当前翻译单元可见。
  • 模块链接(Module Linkage):模块内非导出声明,仅同模块可见。

2.6 全局模块片段(Global Module Fragment)

定义 3.6(全局模块片段):全局模块片段是模块单元中用于包含传统头文件的特殊区域:

ModuleUnit≜GMF⋅ModuleDeclaration⋅ModuleBody\text{ModuleUnit} \triangleq \text{GMF} \cdot \text{ModuleDeclaration} \cdot \text{ModuleBody}

其中 GMF\text{GMF} 是 module; 与 export module M; 之间的区域。

作用:容纳必须保留为头文件包含的代码(如第三方库头文件),这些头文件中的声明属于”全局模块”而非当前模块,因此宏不会泄漏到模块外部。

2.7 私有模块片段(Private Module Fragment)

定义 3.7(私有模块片段):私有模块片段是模块接口单元中用于隐藏实现细节的区域:

ModuleInterfaceUnit≜ModuleDeclaration⋅ExportedDecls⋅module :private;⋅PrivateImpl\text{ModuleInterfaceUnit} \triangleq \text{ModuleDeclaration} \cdot \text{ExportedDecls} \cdot \text{module :private;} \cdot \text{PrivateImpl}

作用:允许将接口与实现放在同一文件中,实现部分对客户端不可见。

限制:

  • 每个模块接口单元最多一个私有模块片段。
  • 私有片段中的声明具有模块链接。
  • 客户端无法看到私有片段的实现细节。

2.8 头文件单元(Header Unit)

定义 3.8(头文件单元):头文件单元是头文件的模块化包装,通过 import <header>; 或 import "header.h"; 导入:

HeaderUnit(h)≜BMI(h) containing all declarations of h\text{HeaderUnit}(h) \triangleq \text{BMI}(h) \text{ containing all declarations of } h

性质:

  • 头文件单元保留头文件中的宏定义(与模块不同)。
  • 头文件单元避免重复解析,提升编译速度。
  • 头文件单元是迁移遗留代码的过渡方案。

3. 理论推导与证明

3.1 模块编译时间的渐近分析

定理 4.1(模块编译时间):使用模块的项目的编译时间渐近优于使用头文件的项目。

证明:设项目有 NN 个翻译单元,每个翻译单元依赖 MM 个模块/头文件。

头文件方案:

  • 每个头文件被每个翻译单元解析一次。
  • 总解析次数:N×MN \times M。
  • 编译时间:O(N×M×∣h∣)O(N \times M \times |h|),其中 ∣h∣|h| 是头文件平均大小。

模块方案:

  • 每个模块只编译一次,生成 BMI。
  • 每个翻译单元导入 BMI(解析 BMI 比 parse 源码快得多)。
  • 编译时间:O(M×∣m∣+N×∣BMI∣)O(M \times |m| + N \times |BMI|),其中 ∣m∣|m| 是模块源码大小,∣BMI∣|BMI| 是 BMI 大小(通常远小于 ∣m∣|m|)。

由于 ∣BMI∣≪∣m∣|BMI| \ll |m|,模块方案在 NN 较大时显著优于头文件方案。□\square

实测数据(LLVM 项目,2024):

  • 头文件方案:完整编译约 45 分钟。
  • 模块方案:完整编译约 15 分钟(3 倍加速)。
  • 增量编译(修改一个头文件/模块):头文件方案约 10 分钟,模块方案约 30 秒(20 倍加速)。

3.2 模块导入的幂等性

定理 4.2(导入幂等性):多次导入同一模块等价于一次导入。

证明:模块的 BMI 是静态的二进制接口,导入操作是”读取 BMI 并注册符号”。重复导入同一 BMI 不会引入新符号(符号唯一性由模块名保证)。

import math;
import math;  // 无副作用,等价于一次导入
import math;  // 同上

C++ 标准规定:导入声明是幂等的,编译器自动去重。□\square

对比:头文件的 #include 不是幂等的(需要 include guard 或 #pragma once),重复包含会导致重复定义错误。

3.3 模块导入的非传递性

定理 4.3(导入非传递性):模块 AA 导入模块 BB,不会自动让导入 AA 的客户端看到 BB 的导出符号。

证明:考虑:

// module_b.cppm
export module B;
export void b_func();

// module_a.cppm
export module A;
import B;  // A 内部可见 b_func
export void a_func() { b_func(); }  // A 的实现使用 B

// client.cpp
import A;
// b_func();  // 错误:B 未被客户端导入
a_func();  // 正确:A 导出了 a_func

客户端只看到 AA 的导出符号,看不到 AA 导入的 BB 的符号。这是模块的封装性保证。

例外:若 AA 使用 export import B;(重新导出),则客户端可以看到 BB 的导出符号:

export module A;
export import B;  // 重新导出 B

// client.cpp
import A;
b_func();  // 正确:A 重新导出了 B

□\square

3.4 模块链接的封装性

定理 4.4(模块链接封装):模块内非导出的声明不会泄漏到模块外部。

证明:考虑:

// math.cppm
export module math;
int internal_helper(int x) { return x * 2; }  // 模块链接
export int square(int x) { return internal_helper(x) / 2 * x; }

// client.cpp
import math;
// internal_helper(5);  // 错误:internal_helper 未导出,不可见
square(5);  // 正确

internal_helper 具有模块链接,仅在同模块内可见。客户端无法访问,实现了真正的封装。□\square

对比:头文件方案中,internal_helper 即使放在 detail 命名空间,也会被所有包含该头文件的翻译单元看到,无法真正封装。

3.5 模块与宏的隔离

定理 4.5(宏隔离):模块内的宏定义不会泄漏到导入该模块的客户端。

证明:考虑:

// config.cppm
export module config;
#define MAX_BUFFER 1024  // 模块内宏
export int get_max_buffer();

// client.cpp
import config;
// int buf[MAX_BUFFER];  // 错误:MAX_BUFFER 未定义
int buf[1024];  // 正确

模块内的宏属于”预处理阶段”的产物,仅存在于该翻译单元的预处理过程中。模块的 BMI 只包含语义信息(声明、类型),不包含预处理产物(宏)。

例外:全局模块片段中的宏会通过头文件单元传递,但不通过模块接口传递。□\square

3.6 模块与模板的交互

定理 4.6(模块模板实例化):模块中的模板定义只实例化一次,避免重复实例化。

证明:考虑:

// container.cppm
export module container;
export template<typename T>
class Vector { /* 完整定义 */ };

// a.cpp
import container;
Vector<int> v1;  // 实例化 Vector<int>

// b.cpp
import container;
Vector<int> v2;  // 复用 a.cpp 的实例化(通过 BMI)

模块的 BMI 中记录了模板的实例化信息,编译器可以跨翻译单元复用实例化,避免重复工作。

对比:头文件方案中,每个包含头文件的翻译单元都会独立实例化 Vector<int>,导致编译时间增加与代码膨胀(虽然链接器会去重)。□\square


4. 代码示例

4.1 基本模块:定义与导入

// file: math.cppm
// 模块接口单元
export module math;

// 导出函数
export int add(int a, int b) { return a + b; }
export int subtract(int a, int b) { return a - b; }

// 非导出函数(模块私有)
int internal_helper(int x) { return x * 2; }

// 导出函数使用私有函数
export int double_and_add(int x, int y) {
    return internal_helper(x) + y;
}
// file: main.cpp
// 客户端使用模块
import math;
#include <iostream>

int main() {
    std::cout << "3 + 4 = " << add(3, 4) << "\n";        // 7
    std::cout << "10 - 6 = " << subtract(10, 6) << "\n"; // 4
    std::cout << "double_and_add(3, 4) = "
              << double_and_add(3, 4) << "\n";           // 10
    // internal_helper(5);  // 错误:未导出
    return 0;
}

编译命令(GCC):

# 编译模块接口单元,生成 .gcm 文件
g++ -std=c++20 -fmodules-ts -c math.cppm

# 编译客户端
g++ -std=c++20 -fmodules-ts main.cpp math.o -o main

编译命令(MSVC):

# MSVC 自动处理模块依赖
cl /std:c++20 /EHsc /c math.cppm
cl /std:c++20 /EHsc main.cpp math.obj

4.2 模块接口与实现分离

// file: calculator.cppm
// 模块接口单元:仅声明
export module calculator;

export int add(int a, int b);
export int subtract(int a, int b);
export double divide(double a, double b);
export class Calculator;
// file: calculator.cpp
// 模块实现单元
module calculator;

int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
double divide(double a, double b) { return a / b; }

class Calculator {
public:
    int add(int a, int b) { return ::add(a, b); }
    int subtract(int a, int b) { return ::subtract(a, b); }
};
// file: main.cpp
import calculator;
#include <iostream>

int main() {
    Calculator calc;
    std::cout << calc.add(5, 3) << "\n";
    return 0;
}

4.3 模块分区

// file: math_core.cppm
// 核心分区
export module math:core;

export int add(int a, int b) { return a + b; }
export int subtract(int a, int b) { return a - b; }
// file: math_advanced.cppm
// 高级分区
export module math:advanced;

export double power(double base, int exp) {
    double result = 1.0;
    for (int i = 0; i < exp; ++i) result *= base;
    return result;
}

export double sqrt(double x) {
    double guess = x / 2;
    for (int i = 0; i < 10; ++i) {
        guess = (guess + x / guess) / 2;
    }
    return guess;
}
// file: math.cppm
// 主模块接口:聚合所有分区
export module math;

// 重新导出分区
export import :core;
export import :advanced;
// file: main.cpp
import math;
#include <iostream>

int main() {
    std::cout << "add: " << add(3, 4) << "\n";
    std::cout << "power: " << power(2, 10) << "\n";
    std::cout << "sqrt: " << sqrt(2) << "\n";
    return 0;
}

4.4 私有模块片段

// file: widget.cppm
// 单文件包含接口与实现
export module widget;

// 导出接口
export class Widget {
public:
    Widget();
    ~Widget();
    void show();
    void hide();
    bool is_visible() const;

private:
    class Impl;  // 前向声明
    Impl* impl_;  // pimpl 惯用法
};

// 私有模块片段:实现细节对客户端不可见
module :private;

// 实际实现
class Widget::Impl {
public:
    bool visible = false;
    int x = 0, y = 0;
    int width = 100, height = 50;
};

Widget::Widget() : impl_(new Impl()) {}
Widget::~Widget() { delete impl_; }

void Widget::show() { impl_->visible = true; }
void Widget::hide() { impl_->visible = false; }
bool Widget::is_visible() const { return impl_->visible; }
// file: main.cpp
import widget;
#include <iostream>

int main() {
    Widget w;
    w.show();
    std::cout << "visible: " << std::boolalpha << w.is_visible() << "\n";
    w.hide();
    std::cout << "visible: " << w.is_visible() << "\n";
    return 0;
}

4.5 全局模块片段

// file: utils.cppm
// 全局模块片段:包含传统头文件
module;  // 全局模块片段开始

// 头文件中的声明属于"全局模块",不属于当前模块
// 但头文件中的宏不会泄漏到模块外部
#include <string>
#include <vector>
#include <iostream>
#include <algorithm>

export module utils;

// 可以使用头文件中的类型
export void print_vector(const std::vector<int>& vec) {
    for (const auto& v : vec) {
        std::cout << v << " ";
    }
    std::cout << "\n";
}

export std::vector<int> sort_vector(std::vector<int> vec) {
    std::sort(vec.begin(), vec.end());
    return vec;
}

export std::string to_string(int x) {
    return std::to_string(x);
}

4.6 导出类与模板

// file: container.cppm
export module container;

#include <memory>
#include <initializer_list>

// 导出完整类
export class Stack {
    struct Node {
        int data;
        std::unique_ptr<Node> next;
    };
    std::unique_ptr<Node> top_;
    size_t size_ = 0;

public:
    Stack() = default;
    ~Stack() = default;

    Stack(const Stack&) = delete;
    Stack& operator=(const Stack&) = delete;

    Stack(Stack&&) noexcept = default;
    Stack& operator=(Stack&&) noexcept = default;

    void push(int value) {
        auto node = std::make_unique<Node>();
        node->data = value;
        node->next = std::move(top_);
        top_ = std::move(node);
        ++size_;
    }

    int pop() {
        if (!top_) return -1;
        int value = top_->data;
        top_ = std::move(top_->next);
        --size_;
        return value;
    }

    bool empty() const { return !top_; }
    size_t size() const { return size_; }
};

// 导出模板类
export template<typename T>
class Queue {
    struct Node {
        T data;
        std::unique_ptr<Node> next;
    };
    std::unique_ptr<Node> head_;
    Node* tail_ = nullptr;
    size_t size_ = 0;

public:
    void push(T value) {
        auto node = std::make_unique<Node>();
        node->data = std::move(value);
        Node* raw = node.get();
        if (tail_) {
            tail_->next = std::move(node);
        } else {
            head_ = std::move(node);
        }
        tail_ = raw;
        ++size_;
    }

    T pop() {
        if (!head_) return T{};
        T value = std::move(head_->data);
        head_ = std::move(head_->next);
        if (!head_) tail_ = nullptr;
        --size_;
        return value;
    }

    bool empty() const { return !head_; }
    size_t size() const { return size_; }
};

4.7 头文件单元

// file: legacy_utils.h
#pragma once
#include <string>

namespace legacy {
    inline std::string greet(const std::string& name) {
        return "Hello, " + name + "!";
    }

    // 宏定义(头文件单元会保留宏)
    #define LEGACY_VERSION "1.0.0"
}
// file: modern_client.cpp
// 使用头文件单元导入传统头文件
import "legacy_utils.h";
#include <iostream>

int main() {
    std::cout << legacy::greet("World") << "\n";
    std::cout << "Version: " << LEGACY_VERSION << "\n";  // 宏可用
    return 0;
}

编译命令(Clang):

# 将头文件转为头文件单元
clang++ -std=c++20 -fmodules -fmodule-header=legacy_utils.h legacy_utils.h

# 编译客户端
clang++ -std=c++20 -fmodules modern_client.cpp -o client

4.8 标准库模块(C++23)

// file: std_module_demo.cpp
// C++23:导入标准库模块
import std;
import std.compat;  // 包含 C 兼容头文件

int main() {
    std::cout << "Hello from std module!\n";
    std::vector<int> v = {1, 2, 3, 4, 5};
    std::ranges::sort(v);

    // C 兼容函数
    printf("Sum: %d\n", std::accumulate(v.begin(), v.end(), 0));
    return 0;
}

编译命令(GCC 14+):

g++ -std=c++23 -fmodules-ts std_module_demo.cpp -o demo

4.9 模块与命名空间组合

// file: network.cppm
export module network;

// 导出整个命名空间
export namespace network::http {
    struct Request {
        std::string url;
        std::string method;
        std::vector<std::pair<std::string, std::string>> headers;
    };

    struct Response {
        int status;
        std::string body;
        std::vector<std::pair<std::string, std::string>> headers;
    };
}

// 模块内部命名空间(不导出)
namespace network::detail {
    bool validate_url(const std::string& url) {
        return !url.empty() && url.substr(0, 4) == "http";
    }

    std::string to_lower(const std::string& s) {
        std::string result = s;
        std::transform(result.begin(), result.end(), result.begin(), ::tolower);
        return result;
    }
}

// 导出函数使用内部命名空间
export namespace network::http {
    bool send_request(const Request& req, Response& resp) {
        if (!detail::validate_url(req.url)) {
            return false;
        }
        // 实际发送逻辑...
        resp.status = 200;
        resp.body = "OK";
        return true;
    }
}

4.10 模块与显式实例化

// file: container.cppm
export module container;

export template<typename T>
class Vector {
    T* data_;
    size_t size_;
    size_t capacity_;
public:
    Vector() : data_(nullptr), size_(0), capacity_(0) {}
    ~Vector() { delete[] data_; }

    void push_back(const T& value) {
        if (size_ >= capacity_) {
            capacity_ = capacity_ == 0 ? 1 : capacity_ * 2;
            T* new_data = new T[capacity_];
            for (size_t i = 0; i < size_; ++i) {
                new_data[i] = std::move(data_[i]);
            }
            delete[] data_;
            data_ = new_data;
        }
        data_[size_++] = value;
    }

    T& operator[](size_t i) { return data_[i]; }
    const T& operator[](size_t i) const { return data_[i]; }
    size_t size() const { return size_; }
};

// 显式实例化声明,减少编译时间
export template class Vector<int>;
export template class Vector<double>;
export template class Vector<std::string>;

5. 对比分析

5.1 模块 vs 头文件

特性头文件 (#include)模块 (import)
编译方式文本包含,每次重新解析预编译 BMI,只解析一次
宏泄漏包含的头文件宏会泄漏模块内宏不泄漏
符号可见性所有声明均可见仅导出符号可见
重复包含需要头文件卫士天然避免重复
编译依赖修改头文件导致全部重编译接口不变则无需重编译
编译顺序无严格依赖(文本包含)必须先编译依赖的模块
跨编译器兼容BMI 格式不兼容
工具支持完善(所有构建系统)仍在完善中(CMake 3.28+)

5.2 模块 vs Rust 的 mod

特性C++ 模块Rust mod
声明语法export module M;mod M;
文件关联显式声明隐式(文件路径对应模块路径)
可见性export 关键字pub 关键字
嵌套分区 M:part嵌套 mod M::part
编译单元模块接口单元crate
跨文件模块分区文件系统层级

Rust 示例:

// lib.rs
pub mod math;
pub mod network;

// math/mod.rs
pub mod core;
pub mod advanced;

pub use core::add;
pub use advanced::power;

Rust 的模块系统与文件路径强绑定,更简洁但灵活性较低。C++ 模块显式声明模块名,允许跨文件组织。

5.3 模块 vs Python 的 import

特性C++ 模块Python import
执行时机编译期运行期
缓存BMI 文件sys.modules 字典
循环导入不允许允许(但有警告)
动态加载不支持支持(importlib)
类型检查编译期运行期

Python 的 import 是运行时操作,模块首次导入时执行模块代码。C++ 的 import 是编译时操作,模块的 BMI 在编译期生成。

5.4 模块 vs Java 的 package

特性C++ 模块Java package
组织方式逻辑单元目录结构
可见性exportpublic/protected/private
编译单元模块接口单元.java 文件
打包BMI + 对象文件JAR 文件
反射受限(C++26 将改进)完整支持

Java 的 package 是命名空间机制,模块化(Java 9 引入的 Module System)是另一层。C++ 模块将命名空间与编译单元统一管理。

5.5 三大编译器实现对比

特性GCCClangMSVC
BMI 格式.gcm (CMI).pcm (BMI).ifc
模块后缀.cppm.cppm.ixx
编译选项-std=c++20-std=c++20/std:c++20
标准库模块(import std)GCC 15 起较完整改进中(需自行构建 BMI)MSVC 先行者,VS 2022 新版本可用
CMake 支持3.28+3.28+3.28+
跨编译器 BMI不兼容不兼容不兼容

跨编译器策略:

  • 分发源码:客户端用各自编译器编译模块。
  • 分发 BMI:仅限同编译器同版本,不通用。
  • 分发传统头文件 + 静态库:兼容性最好,但失去模块优势。

6. 常见陷阱与反模式

6.1 import 位置错误

反模式:

#include <iostream>
import math;  // 错误:import 必须在 #include 之后、其他声明之前

int main() { /* ... */ }

问题:C++ 标准规定 import 声明必须位于全局模块片段之后、所有其他声明之前(除了 module 声明本身)。

修复:

// 全局模块片段
module;
#include <iostream>
export module my_app;

// 导入其他模块
import math;

// 其他声明
int main() { /* ... */ }

或对于非模块翻译单元:

import math;
#include <iostream>

int main() { /* ... */ }

6.2 在模块中泄漏宏

反模式:

// config.cppm
export module config;

#define MAX_SIZE 1024  // 这个宏不会泄漏到客户端

export int get_max_size() { return MAX_SIZE; }
// client.cpp
import config;
int buf[MAX_SIZE];  // 错误:MAX_SIZE 未定义

问题:模块内的宏定义不会通过 import 传递到客户端。

修复:使用 constexpr 替代宏:

export module config;
export constexpr int MAX_SIZE = 1024;

export int get_max_size() { return MAX_SIZE; }
import config;
int buf[MAX_SIZE];  // 正确

6.3 模块循环依赖

反模式:

// a.cppm
export module A;
import B;  // A 依赖 B

// b.cppm
export module B;
import A;  // B 依赖 A —— 循环依赖!

问题:模块不允许循环依赖,编译器无法确定编译顺序。

修复:重构以消除循环依赖:

// common.cppm
export module common;
// 共享的类型与函数

// a.cppm
export module A;
import common;
// A 依赖 common

// b.cppm
export module B;
import common;
// B 依赖 common

6.4 在全局模块片段中声明模块符号

反模式:

// math.cppm
module;
#include <vector>

class MathHelper {  // 错误:在全局模块片段中声明
public:
    static int square(int x) { return x * x; }
};

export module math;
export int compute(int x) { return MathHelper::square(x); }

问题:全局模块片段中的声明属于”全局模块”,不具有模块链接,且无法被模块接口使用。

修复:将声明移到模块声明之后:

// math.cppm
module;
#include <vector>

export module math;

class MathHelper {  // 现在具有模块链接
public:
    static int square(int x) { return x * x; }
};

export int compute(int x) { return MathHelper::square(x); }

6.5 修改模块接口导致大规模重编译

反模式:

// math.cppm
export module math;

export int add(int a, int b);
export int subtract(int a, int b);
// 添加新函数时插入到中间
export int multiply(int a, int b);  // 新增
export int divide(int a, int b);

问题:修改模块接口(即使只是添加新函数)会导致 BMI 重新生成,所有导入该模块的客户端需要重新编译。

最佳实践:

  • 接口设计稳定后再发布。
  • 新增功能尽量添加到模块末尾。
  • 使用分区管理大型模块,修改分区只影响依赖该分区的客户端。

6.6 混用模块与头文件的歧义

反模式:

// math.h
#pragma once
int add(int a, int b);

// math.cppm
export module math;
export int add(int a, int b);

// client.cpp
#include "math.h"    // 头文件声明
import math;          // 模块声明
// 两个 add 声明,可能产生歧义
int main() { return add(1, 2); }

问题:同时包含头文件与导入模块,可能导致重复声明或歧义。

修复:统一使用模块或头文件,避免混用:

// 方案1:完全使用模块
import math;

// 方案2:完全使用头文件
#include "math.h"

6.7 私有模块片段中的导出

反模式:

// widget.cppm
export module widget;

export class Widget { /* ... */ };

module :private;

export void hidden_function();  // 错误:私有片段中不能 export

问题:私有模块片段中的声明不可导出。

修复:将导出声明移到私有片段之前:

export module widget;

export class Widget { /* ... */ };
export void hidden_function();  // 导出声明

module :private;

void hidden_function() { /* 实现 */ }  // 实现细节

6.8 跨编译器 BMI 不兼容

反模式:

# 用 GCC 编译模块
g++ -std=c++20 -fmodules-ts -c math.cppm  # 生成 math.gcm

# 用 Clang 编译客户端,尝试使用 GCC 的 BMI
clang++ -std=c++20 -fmodules main.cpp  # 错误:无法读取 .gcm

问题:不同编译器的 BMI 格式不兼容。

修复:每个编译器单独编译模块:

# GCC 编译
g++ -std=c++20 -fmodules-ts -c math.cppm
g++ -std=c++20 -fmodules-ts main.cpp math.o -o main_gcc

# Clang 编译
clang++ -std=c++20 -fmodules -c math.cppm
clang++ -std=c++20 -fmodules main.cpp math.o -o main_clang

7. 工程实践与最佳实践

7.1 模块化项目结构

推荐的模块化项目结构:

flowchart TD
    T0["my_project/"]
    T1["CMakeLists.txt"]
    T2["src/"]
    T3["core/"]
    T4["core.cppm           # 主模块接口"]
    T5["core_algorithm.cppm # 分区:算法"]
    T6["core_container.cppm # 分区:容器"]
    T7["core_io.cppm        # 分区:IO"]
    T8["utils/"]
    T9["utils.cppm"]
    T10["utils_string.cppm"]
    T11["app/"]
    T12["main.cpp"]
    T13["tests/"]
    T14["test_core.cpp"]
    T0 --> T1
    T0 --> T2
    T12 --> T13
    T13 --> T14

7.2 CMake 模块配置

# CMakeLists.txt
cmake_minimum_required(VERSION 3.28)
project(my_project CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 定义模块文件集
add_library(my_core STATIC)
target_sources(my_core
    PUBLIC
    FILE_SET CXX_MODULES FILES
        src/core/core.cppm
        src/core/core_algorithm.cppm
        src/core/core_container.cppm
        src/core/core_io.cppm
)

target_compile_features(my_core PUBLIC cxx_std_20)

# 客户端
add_executable(my_app src/app/main.cpp)
target_link_libraries(my_app PRIVATE my_core)

7.3 模块分层架构

// 层次1:基础工具模块
// utils.cppm
export module utils;
import std;
export std::string trim(const std::string& s);
export std::string to_lower(const std::string& s);

// 层次2:核心模块(依赖 utils)
// core.cppm
export module core;
import utils;
import std;
export class Document {
    std::string content_;
public:
    void set_content(const std::string& s) { content_ = trim(s); }
    std::string get_content() const { return to_lower(content_); }
};

// 层次3:应用模块(依赖 core)
// app.cppm
export module app;
import core;
import std;
export class Editor {
    Document doc_;
public:
    void open(const std::string& path) {
        // 读取文件到 doc_
    }
    void save(const std::string& path) {
        // 保存 doc_ 到文件
    }
};

7.4 从头文件迁移到模块

分阶段迁移策略:

阶段1:引入头文件单元

// 不修改现有头文件,仅将 #include 改为 import "header.h"
import "my_header.h";  // 替代 #include "my_header.h"

阶段2:将独立头文件转为模块

// 旧:utils.h
#pragma once
#include <string>
std::string trim(const std::string& s);

// 新:utils.cppm
export module utils;
import std;
export std::string trim(const std::string& s);

阶段3:处理依赖关系

// core.h 依赖 utils.h
// 旧:core.h
#pragma once
#include "utils.h"
class Core { /* 使用 utils 的函数 */ };

// 新:core.cppm
export module core;
import utils;
export class Core { /* 使用 utils 的函数 */ };

阶段4:消除循环依赖

重构代码,使用中间模块打破循环。

7.5 模块与第三方库

// file: third_party_wrapper.cppm
module;
#include <nlohmann/json.hpp>  // 第三方库,大量宏
#include <boost/asio.hpp>     // 第三方库

export module third_party_wrapper;

// 仅导出需要的类型,宏不会泄漏
export using json = nlohmann::json;

export namespace my_wrapper {
    using boost::asio::io_context;
    using boost::asio::ip::tcp;

    class TcpClient {
        io_context& io_;
        tcp::socket socket_;
    public:
        explicit TcpClient(io_context& io) : io_(io), socket_(io) {}
        void connect(const std::string& host, int port);
    };
}

7.6 模块与模板库

// file: template_lib.cppm
export module template_lib;
import std;

// 导出模板(定义可见,支持隐式实例化)
export template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

export template<Numeric T>
class Matrix {
    std::vector<T> data_;
    size_t rows_, cols_;
public:
    Matrix(size_t r, size_t c) : data_(r * c), rows_(r), cols_(c) {}

    T& operator()(size_t i, size_t j) { return data_[i * cols_ + j]; }
    const T& operator()(size_t i, size_t j) const { return data_[i * cols_ + j]; }

    Matrix operator+(const Matrix& other) const {
        Matrix result(rows_, cols_);
        for (size_t i = 0; i < data_.size(); ++i) {
            result.data_[i] = data_[i] + other.data_[i];
        }
        return result;
    }
};

// 显式实例化常用类型
export template class Matrix<int>;
export template class Matrix<double>;

7.7 模块测试

// file: math.cppm
export module math;
export int factorial(int n);

// file: math.cpp
module math;
int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}

// file: test_math.cpp
import math;
import std;

int main() {
    assert(factorial(0) == 1);
    assert(factorial(1) == 1);
    assert(factorial(5) == 120);
    assert(factorial(10) == 3628800);
    std::cout << "All tests passed!\n";
    return 0;
}

8. 案例研究

8.1 案例一:LLVM 的模块化迁移

LLVM 项目自 2020 年开始迁移到 C++20 模块,是最大规模的模块化迁移案例之一。

迁移策略:

  1. 使用 Build2 构建系统(原生支持模块)。
  2. 分阶段迁移,从头文件单元开始。
  3. 处理循环依赖(LLVM 内部模块之间存在复杂依赖)。

成果:

  • 编译时间从 45 分钟降至 15 分钟。
  • 增量编译速度提升 20 倍。
  • 宏污染问题大幅减少。

经验教训:

  • 模块化迁移是长期工程,LLVM 花费 3 年逐步完成。
  • 头文件单元是良好的过渡方案。
  • 构建系统支持是关键瓶颈。

8.2 案例二:Google 内部的模块实践

Google 自 2017 年起在内部大规模使用 Clang 的 -fmodules-ts 实现模块化,早于 C++20 标准化。

设计特点:

  • 使用自动生成的模块映射文件(module.modulemap)。
  • 与 Bazel 构建系统深度集成。
  • 模块化数千个内部库。

成果:

  • 编译时间减少 40%。
  • 宏冲突问题基本消除。
  • 增量开发效率显著提升。

经验教训:

  • 模块映射文件的维护是挑战。
  • 自动化工具支持至关重要。
  • 模块化推动了代码架构的清晰化。

8.3 案例三:Qt 6 的模块化

Qt 6 部分采用了 C++20 模块,将核心库组织为模块。

// Qt 6 模块示例
import Qt.Core;
import Qt.Gui;
import Qt.Widgets;

int main(int argc, char* argv[]) {
    QApplication app(argc, argv);
    QPushButton button("Hello, Qt Modules!");
    button.show();
    return app.exec();
}

设计特点:

  • 每个 Qt 模块(Core、Gui、Widgets 等)对应一个 C++20 模块。
  • 保留传统头文件以兼容旧代码。
  • 使用 CMake 的 FILE_SET CXX_MODULES 配置。

成果:

  • 编译时间减少 30%。
  • 头文件污染问题缓解。
  • 平滑兼容旧代码。

8.4 案例四:Boost 的模块化探索

Boost 库正在探索模块化方案,但进展缓慢。

挑战:

  • Boost 包含 150+ 个独立库,依赖关系复杂。
  • 许多 Boost 库重度使用宏,与模块设计冲突。
  • 跨编译器兼容性要求高。

策略:

  • Boost.Math 等小型库先迁移。
  • 提供 import boost.math; 与传统 #include <boost/math.hpp> 双轨支持。
  • 长期目标是全 Boost 模块化。

8.5 案例五:Visual Studio 的模块支持

MSVC 自 Visual Studio 2019 16.7 起支持 C++20 模块,是商业 IDE 中最早支持的。

特性:

  • 原生支持 .ixx 模块接口文件。
  • IntelliSense 支持模块导入。
  • 项目模板支持模块化项目。

示例项目结构:

flowchart TD
    T0["MyModuleApp/"]
    T1["MyModuleApp.sln"]
    T2["MyModuleApp.vcxproj"]
    T3["math/"]
    T4["math.ixx          # 模块接口"]
    T5["math.cpp          # 模块实现"]
    T6["src/"]
    T7["main.cpp"]
    T0 --> T1
    T0 --> T2
    T0 --> T3
    T5 --> T6
    T6 --> T7

8.6 案例六:标准库模块(C++23)

C++23 标准化了标准库模块,是模块化的重要里程碑。

// C++23 标准库模块
import std;           // 标准库
import std.compat;    // 标准库 + C 兼容头文件

int main() {
    std::cout << "Hello, std module!\n";
    std::vector v = {1, 2, 3};
    std::ranges::sort(v);
    return 0;
}

成果:

  • 编译时间减少 50%(相比 #include <iostream> 等)。
  • 消除 min/max 宏冲突。
  • 简化标准库使用。

编译器支持(总体仍滞后于标准,落地细节随版本变化大,使用前请查证):

  • MSVC:import std; 的先行者,VS 2022 较新版本配合 vcpkg 清单模式或 /scanDependencies 可用性最好。
  • GCC:GCC 15 起 import std 较为完整(GCC 14 仅有初步支持)。
  • Clang/libc++:持续改进中,早期版本需要自行构建标准库模块 BMI,生产使用前务必验证。

11.1 标准与规范

  • C++ Standard [module.unit], [module.import], [module.global.frag]:标准对模块单元、导入声明、全局模块片段的规定。
  • P1103 (Modules):C++20 模块提案,包含设计动机与权衡。
  • P1689 (Module Dependencies):构建系统的模块依赖发现格式。
  • P0965 (Standard Library Modules):标准库模块化提案。

11.2 编译器实现

  • GCC Modules:gcc/cp/module.cc 中模块的生成与导入逻辑。
  • Clang Modules:clang/lib/Serialization/ModuleFile.cpp 中 BMI 的序列化。
  • MSVC Modules:通过 /d1module.json 选项查看模块依赖。

11.3 构建系统支持

  • CMake 3.28+:FILE_SET CXX_MODULES 原生支持模块。
  • Build2:专为模块设计的构建系统。
  • Ninja:原生支持模块依赖图。
  • Bazel:Google 的构建系统,支持 Clang 模块。

11.4 相关技术

  • 头文件单元(Header Units):迁移遗留代码的过渡方案。
  • Pimpl 惯用法:与私有模块片段结合,实现接口与实现分离。
  • 模块与模板:模块如何解决模板的重复实例化问题。
  • 模块与 ABI:修改模块接口对 ABI 兼容性的影响。

11.5 未来方向

  • C++26 反射(P2996):反射与模块的结合,支持运行时查询模块信息。
  • 模块 ABI 稳定性(P2874):提案讨论模块的 ABI 兼容性保证。
  • 跨编译器 BMI 格式:未来可能标准化 BMI 格式,实现跨编译器互操作。
  • 模块化标准库扩展:C++26 可能引入更多标准库模块(如 import std.compat 的扩展)。

11.6 附录

附录 A:术语表

术语英文定义
模块ModuleC++20 引入的代码组织单元
模块单元Module Unit包含模块声明的翻译单元
模块接口单元Module Interface Unit声明 export module 的单元
模块实现单元Module Implementation Unit声明 module(无 export)的单元
模块分区Module Partition模块内部的子单元
全局模块片段Global Module Fragment容纳头文件包含的特殊区域
私有模块片段Private Module Fragment隐藏实现细节的区域
头文件单元Header Unit头文件的模块化包装
BMIBinary Module Interface预编译的模块二进制接口
模块链接Module Linkage模块内非导出声明的链接属性

附录 B:模块单元后缀约定

编译器模块接口模块实现BMI
GCC.cppm.cpp.gcm
Clang.cppm.cpp.pcm
MSVC.ixx.cpp.ifc

附录 C:模块迁移决策树

flowchart TD
    T0["是否是新项目?"]
    T1["是 → 直接使用 C++20 模块"]
    T2["否 → 是否能接受破坏性变更?"]
    T3["是 → 全面迁移到模块"]
    T4["否 → 分阶段迁移"]
    T5["阶段1:引入头文件单元"]
    T6["阶段2:将独立头文件转为模块"]
    T7["阶段3:处理依赖关系"]
    T8["阶段4:消除循环依赖"]
    T9["是否需要跨编译器?"]
    T10["是 → 分发源码,客户端各自编译"]
    T11["否 → 分发 BMI + 静态库"]
    T0 --> T1
    T0 --> T2
    T2 --> T3
    T2 --> T4
    T4 --> T5
    T4 --> T6
    T4 --> T7
    T4 --> T8
    T8 --> T9
    T9 --> T10
    T9 --> T11

模块声明

基本写法:声明一个模块接口单元 export module <模块名>;

// 定义名为 math 的模块接口
export module math;

基本写法:声明模块分区 module <模块名>:<分区名>;

// 模块 math 的内部实现分区
module math:impl;

基本写法:声明模块实现单元 module <模块名>;

// 模块 math 的实现单元,不导出声明
module math;

导出声明

基本写法:导出函数 export <返回类型> <函数名>(<参数>);

// 导出加法函数供外部使用
export int add(int a, int b);

基本写法:导出类 export class <类名> { };

// 导出整个类
export class Calculator {
public:
    int sub(int a, int b);
};

基本写法:导出命名空间 export namespace <命名空间名> { }

// 导出整个命名空间
export namespace geo {
    double pi = 3.14159;
    double area(double r);
}

基本写法:分组导出 export { <声明1>; <声明2>; }

// 一次性导出多个声明
export {
    int mul(int a, int b);
    int div(int a, int b);
}

导入模块

基本写法:导入模块 import <模块名>;

// 导入 math 模块以使用其导出内容
import math;

基本写法:导入头文件单元 import <头文件名>;

// 将头文件作为模块单元导入
import <iostream>;

基本写法:全局模块片段声明头文件 module; <头文件包含> export module <模块名>;

// 全局片段中包含传统头文件
module;
#include <cstdio>
export module logger;

模块分区组合

基本写法:导入本模块分区 import :<分区名>;

// 在主接口中导入分区
export module math;
import :impl;

基本写法:导出分区 export import :<分区名>;

// 将分区的导出内容重新导出
export module math;
export import :core;

编译与使用

基本写法:编译模块接口 g++ -std=c++20 -fmodules-ts -c <文件>.cpp

// 编译模块接口单元生成 gcm 文件
g++ -std=c++20 -fmodules-ts -c math.cpp

基本写法:MSVC 编译模块 cl /std:c++20 /c /interface <文件>.cpp

// MSVC 编译模块接口单元
cl /std:c++20 /c /interface math.cpp