前置知识: C++C++C++

C++20模块

42 minIntermediate2026/7/21

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

C++20 模块(Modules)

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


目录

  1. 学习目标
  2. 历史动机与演化
  3. 形式化定义
  4. 理论推导与证明
  5. 代码示例
  6. 对比分析
  7. 常见陷阱与反模式
  8. 工程实践与最佳实践
  9. 案例研究
  10. 习题与思考题
  11. 参考文献
  12. 延伸阅读

1. 学习目标

本章节遵循 Bloom 分类法(Bloom’s Taxonomy)设计学习目标,自低阶认知向高阶创造逐级递进。完成本章节后,读者应能够:

1.1 记忆(Remembering)

  • R1:复述模块(Module)的核心三要素:模块接口单元(export module)、模块实现单元(module)、模块分区(module :part)。
  • R2:列出模块相对头文件的五大优势:编译速度、宏隔离、符号封装、重复包含避免、依赖稳定。
  • R3:背诵 import 声明的语法与位置约束:必须位于全局模块片段之后、所有其他声明之前。
  • R4:识别 export 关键字的三种用法:导出模块、导出声明、重新导出分区(export import)。

1.2 理解(Understanding)

  • U1:解释模块的”预编译二进制接口”(BMI/CMI)机制,阐明为何模块只需编译一次即可被多次导入。
  • U2:阐明全局模块片段(Global Module Fragment)的作用:容纳必须保留为头文件包含的代码(如第三方库),避免宏泄漏。
  • U3:对比模块单元(Module Unit)与头文件单元(Header Unit)的差异:前者是原生模块,后者是头文件的模块化包装。
  • U4:说明模块的链接模型:模块内非导出符号具有模块链接(Module Linkage),不泄漏到外部翻译单元。

1.3 应用(Applying)

  • A1:使用 export module 定义模块接口单元,使用 import 在客户端导入。
  • A2:使用模块分区(Partition)将大型模块拆分为多个文件,通过 export import :part 聚合。
  • A3:使用私有模块片段(Private Module Fragment)将接口与实现放在同一文件中。
  • A4:使用 CMake 的 FILE_SET CXX_MODULES 或 Build2 配置模块的构建系统。

1.4 分析(Analyzing)

  • An1:分析以下代码的编译过程,指出模块依赖图与编译顺序:
    // math.cppm
    export module math;
    export import :core;
    export import :advanced;
    绘制模块依赖图,说明为何 math:core 必须在 math 之前编译。
  • An2:解构模块与模板显式实例化的交互,分析模块如何解决模板的”重复实例化”问题。
  • An3:对比 C++ 模块与 Rust 的 mod、Python 的 import、Java 的 package,指出各自的编译模型差异。

1.5 评价(Evaluating)

  • E1:评价”模块 vs 头文件”之争,给出在新建项目、遗留代码库、跨编译器库三类场景中的迁移策略。
  • E2:批判性分析 C++ 模块标准化过程中的争议:模块单元后缀(.cppm vs .ixx)、BMI 格式跨编译器不兼容、构建系统支持滞后。
  • E3:评估模块对 ABI 兼容性的影响:修改模块接口会导致 BMI 重新生成,下游需重新编译,比头文件更严格。

1.6 创造(Creating)

  • C1:设计一个使用模块分层架构的中型项目,将核心库、工具库、应用层分别组织为模块,支持增量编译与库分发。
  • C2:实现一个跨编译器的模块构建脚本,统一处理 GCC 的 .gcm、Clang 的 .pcm、MSVC 的 .ifc 文件。
  • C3:构建一个模块化的大型遗留代码迁移方案,制定分阶段迁移计划,处理循环依赖与宏依赖。

2. 历史动机与演化

2.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 中的任何声明(即使是非公开的私有函数)
// 会导致所有包含该头文件的源文件重新编译

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

2.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);

2.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 声明私有模块片段。

2.4 编译器支持现状(2023-2025)

截至 2025 年,三大编译器的模块支持情况:

编译器版本支持程度BMI 格式后缀约定
GCC14+完整支持.gcm (CMI).cppm
Clang17+完整支持.pcm (PCH).cppm
MSVC19.36+完整支持.ifc.ixx

跨编译器兼容性问题

  • BMI 格式不兼容:GCC 的 .gcm 无法被 Clang 读取。
  • 模块单元后缀不统一:GCC/Clang 用 .cppm,MSVC 用 .ixx
  • 构建系统支持参差不齐:CMake 3.28+ 支持,Ninja 原生支持,Make 需手动配置。

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

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

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

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

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

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

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


3. 形式化定义

3.1 模块单元(Module Unit)

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

ModuleUnitModuleInterfaceUnitModuleImplementationUnitModulePartition\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;,是模块内部的子单元,仅能被同模块的其他单元导入。

3.2 模块(Module)

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

MInterface(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) 是零个或多个模块分区。

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

3.3 导出(Export)

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

export ddInterface(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 声明(隐式)。

3.4 导入(Import)

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

import MdInterface(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 不同)。

3.5 模块链接(Module Linkage)

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

dModule(M),dexported: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):模块内非导出声明,仅同模块可见。

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

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

ModuleUnitGMFModuleDeclarationModuleBody\text{ModuleUnit} \triangleq \text{GMF} \cdot \text{ModuleDeclaration} \cdot \text{ModuleBody}

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

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

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

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

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

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

限制

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

3.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

性质

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

4. 理论推导与证明

4.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|)。

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

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

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

4.2 模块导入的幂等性

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

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

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

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

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

4.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

4.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 命名空间,也会被所有包含该头文件的翻译单元看到,无法真正封装。

4.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

4.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


5. 代码示例

5.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

5.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;
}

5.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;
}

5.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;
}

5.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);
}

5.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_; }
};

5.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

5.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

5.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;
    }
}

5.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>;

6. 对比分析

6.1 模块 vs 头文件

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

6.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++ 模块显式声明模块名,允许跨文件组织。

6.3 模块 vs Python 的 import

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

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

6.4 模块 vs Java 的 package

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

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

6.5 三大编译器实现对比

特性GCCClangMSVC
BMI 格式.gcm (CMI).pcm (PCH).ifc
模块后缀.cppm.cppm.ixx
编译选项-fmodules-ts-fmodules/std:c++20
标准库模块GCC 14+Clang 17+MSVC 19.36+
CMake 支持3.28+3.28+3.28+
跨编译器 BMI不兼容不兼容不兼容

跨编译器策略

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

7. 常见陷阱与反模式

7.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() { /* ... */ }

7.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];  // 正确

7.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

7.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); }

7.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 重新生成,所有导入该模块的客户端需要重新编译。

最佳实践

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

7.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"

7.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() { /* 实现 */ }  // 实现细节

7.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

8. 工程实践与最佳实践

8.1 模块化项目结构

推荐的模块化项目结构:

my_project/
├── CMakeLists.txt
├── src/
│   ├── core/
│   │   ├── core.cppm           # 主模块接口
│   │   ├── core_algorithm.cppm # 分区:算法
│   │   ├── core_container.cppm # 分区:容器
│   │   └── core_io.cppm        # 分区:IO
│   ├── utils/
│   │   ├── utils.cppm
│   │   └── utils_string.cppm
│   └── app/
│       └── main.cpp
└── tests/
    └── test_core.cpp

8.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)

8.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_ 到文件
    }
};

8.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:消除循环依赖

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

8.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);
    };
}

8.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>;

8.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;
}

9. 案例研究

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

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

迁移策略

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

成果

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

经验教训

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

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

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

设计特点

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

成果

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

经验教训

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

9.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%。
  • 头文件污染问题缓解。
  • 平滑兼容旧代码。

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

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

挑战

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

策略

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

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

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

特性

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

示例项目结构

MyModuleApp/
├── MyModuleApp.sln
├── MyModuleApp.vcxproj
├── math/
│   ├── math.ixx          # 模块接口
│   └── math.cpp          # 模块实现
└── src/
    └── main.cpp

9.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 宏冲突。
  • 简化标准库使用。

编译器支持

  • GCC 14+:支持 import std;
  • Clang 17+:支持 import std;
  • MSVC 19.36+:支持 import std;

10. 习题与思考题

10.1 习题

习题 1:将以下头文件代码迁移为模块:

// math_utils.h
#pragma once
#include <vector>

namespace math_utils {
    int sum(const std::vector<int>& v);
    double average(const std::vector<int>& v);

    namespace detail {
        int internal_helper(int x);
    }
}

习题 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];

习题 3:设计一个使用模块分区的图形库,包含以下分区:

  • graphics:core - 基础类型(Point、Color)
  • graphics:shapes - 形状(Circle、Rectangle)
  • graphics:renderer - 渲染器(OpenGL、Vulkan)

习题 4:使用 CMake 配置一个模块化项目,包含 math 模块与 app 可执行文件。

习题 5:分析以下代码的编译顺序:

// a.cppm: export module A; import B;
// b.cppm: export module B; import C;
// c.cppm: export module C;
// main.cpp: import A;

习题 6:将以下使用 #include 的代码迁移为头文件单元,保持宏可用:

#include "config.h"  // 定义 CONFIG_VERSION 宏
#include <iostream>

int main() {
    std::cout << "Version: " << CONFIG_VERSION << "\n";
}

10.2 思考题

思考题 1:为什么 C++ 标准未规定 BMI 的具体格式?这种”实现定义”的灵活性带来了哪些好处与坏处?

思考题 2:模块化迁移的最大障碍是什么?是技术问题(编译器支持)、工程问题(构建系统)还是社会问题(开发者习惯)?

思考题 3:C++ 模块与 Rust 的 mod 系统相比,哪种设计更优?为什么?

思考题 4:如果 C++26 引入反射(P2996),模块与反射的结合会带来哪些新的可能性?例如,能否在运行时查询模块的导出符号列表?


11. 参考文献

  1. ISO/IEC 14882:2020. Information technology — Programming languages — C++. International Organization for Standardization. [module.unit], [module.import], [module.global.frag].
  2. Smith, R. (2018). P1103: Working Draft, C++ Modules. ISO C++ Committee Proposal.
  3. Vandevoorde, D. (2012). Modules for C++. C++Now 2012.
  4. Sutter, H., & Vandenoord, G. (2018). P0965: Standard Library Modules. ISO C++ Committee Proposal.
  5. Lelbach, B. (2023). P1689: Format for describing module dependencies. ISO C++ Committee Proposal.
  6. Stroustrup, B. (2013). The C++ Programming Language (4th ed.). Addison-Wesley. ISBN 978-0321563842.
  7. Meyers, S. (2005). Effective C++ (3rd ed.). Addison-Wesley. ISBN 978-0321334879.
  8. Vandevoorde, D., Josuttis, N. M., & Gregor, D. (2017). C++ Templates: The Complete Guide (2nd ed.). Addison-Wesley. ISBN 978-0321714121.
  9. GCC Manual (2024). C++ Modules. https://gcc.gnu.org/onlinedocs/gcc/C_002b_002b-Modules.html
  10. Clang Documentation (2024). Modules. https://clang.llvm.org/docs/Modules.html
  11. Microsoft Learn (2024). C++ Modules in MSVC. https://learn.microsoft.com/en-us/cpp/cpp/modules-cpp
  12. CMake Documentation (2024). C++ Modules Support. https://cmake.org/cmake/help/latest/manual/cmake-modules.7.html
  13. Build2 Documentation (2024). C++ Modules. https://build2.org/build2/doc/build2-build-system-manual.xhtml#cxx-modules
  14. Lattner, C., & Adve, V. (2004). LLVM: A Compilation Framework for Lifelong Program Analysis & Transformation. CGO 2004.
  15. P2996R5 (2024). Reflection for C++26. ISO C++ Committee Proposal.

12. 延伸阅读

12.1 标准与规范

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

12.2 编译器实现

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

12.3 构建系统支持

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

12.4 相关技术

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

12.5 未来方向

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

12.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:模块迁移决策树

是否是新项目?
├─ 是 → 直接使用 C++20 模块
└─ 否 → 是否能接受破坏性变更?
    ├─ 是 → 全面迁移到模块
    └─ 否 → 分阶段迁移
        ├─ 阶段1:引入头文件单元
        ├─ 阶段2:将独立头文件转为模块
        ├─ 阶段3:处理依赖关系
        └─ 阶段4:消除循环依赖

是否需要跨编译器?
├─ 是 → 分发源码,客户端各自编译
└─ 否 → 分发 BMI + 静态库
返回入门指南