C++20模块化

C++20模块化
https://open.spotify.com/playlist/6zCID88oNjNv9zx6puDHKj?si=2697caec55594412&nd=1&dlsi=1ac4dd1566274a75

现在让我们设好番茄钟放一首好听的音乐开始学习吧 🌈 😋


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 中正常工作,一般需要:

  1. 确保编译器版本 ≥ 19.35
  2. 安装「适用于 v143 生成工具的 C++ 模块(x64/x86,实验性)」组件
  3. 工程属性 → C/C++ → 常规:将「扫描源以查找模块依赖关系」设为
  4. 工程属性 → 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 渐进式迁移策略

在现有项目中引入模块,建议:

  1. 稳定的基础库开始模块化(如工具库、数学库等)
  2. 优先改写「几乎不改动」的头文件,收益最大
  3. 暂时保留 #include 的第三方库,通过全局模块片段兼容
  4. 在新模块接口中只导出最小必要 API,减少耦合

8.2 常见坑位

  • 模板、内联函数应放在接口单元,否则容易出现链接错误
  • 不同编译器、不同版本对模块支持不完全一致:
    • 需要为编译器单独配置模块扫描 / 依赖分析
    • 标准库模块 import std; 的实现细节仍在演进
  • 构建系统层面(CMake / 自建脚本)需要适配「模块文件(如 .pcm)」的生成与依赖(现阶段使用CMake构建还存在一定的难度,跨平台死性和稳定性都还得不到确切的保证,现阶段建议在实验性项目中使用)

 

9. 小结

  • C++20 模块提供了新一代代码组织方式,在语言层面替代了传统的「头文件 + 宏」模式
  • 通过 export module / import / export 等关键字,实现:
    • 更快的编译
    • 更清晰的接口边界
    • 更好的封装和可维护性
  • C++23 开始推动标准库模块化,配合主流编译器,可以逐步在实际工程中实践

🚀 如果你已经在使用 C++17/20,可以从一个小型工具库开始尝试模块化,亲自感受 语义更清楚、工程更干净、编译更快速 的体验。