C++模块的用法(非常详细)
模块是 C++20 中最重要的变化之一。这是 C++ 语言及编写代码、使用代码方式的根本变化。模块在源文件中可用,其从使用它们的编译单元中单独编译。
模块有几个优点,特别跟头文件相比:
在本节中,你将学习如何开始模块的使用。
1) 使用 import 指令导入模块,紧跟着模块名。标准库尽管还没模块化,但可作为编译器特定模块存在。
以下代码片段使用了 Visual C++ 的 std.core 模块,它包含了标准库里大多数功能,包括流式库:
2) 通过创建包含函数、类型、常量和宏的模块接口单元(Module Interface Unit,MIU)来导出模块。它们的声明必须以关键字 export 开头。
VC++ 模块接口单元文件的扩展必须是 .ixx;Clang 接受不同的扩展名,包括 .cpp、.cppm,甚至.ixx。
以下示例导出名为 point 的类模板、用于计算两点距离的 distance() 函数,以及用户自定义字面常量操作符 _ip,其从字符串形式"0,0"或"12,-3"中创建 point 类型的对象:
3) 使用 import 指令导入头文件内容。以下示例使用前一示例所示的同一类型和函数:
下图展示了模块包含之前提及的所有部分。在左边,我们有模块的源码;在右边,我们解释了模块部分。

图 1 模块(左边)示例,每部分高亮且被解释(右边)
模块可导出任何实体,如函数、类和常量。每次导出都以 export 关键字在前面。这个关键字通常是第一个关键字,在其他 class/struct、template 或 using 前。
前面的 geometry 模块中提供了几个示例:
使用模块而不使用头文件的编译单元,除了将 #include 预处理器指令替换为 import 指令外,不需要其他更改。此外,用同样的 import 指令,头文件也可如模块一样导入,就如之前示例所示。
模块和命名空间之间没有关系。这两个是正交的概念。模块 geometry 导出用户自定义字面""_ip到命名空间 geometry_literals,同时模块中其他的导出在全局命名空间可用。
模块名称和单元文件名称也没有关系。尽管任何文件名有同样的效果,geometry 模块在 geometry.ixx/.cppm 文件中定义。建议你遵循一致的命名规范,使用模块名作为模块文件名。另外,模块单元的扩展名因编译器而异,尽管当模块支持成熟后,将来可能会有变化。
标准库还没模块化,尽管这很可能在标准库未来版本中发生。然而,编译器已经将其模块化实现了。Clang 编译器为每个头文件提供了不同的模块。另外,Visual C++ 编译器为标准库提供了以下模块:
如你从这些模块名(如 std.core 或 std.regex)中所见,模块名可以是一系列由点(.)拼接的标识符。点除了将名称分为代表逻辑层次部分外,如 company.project.module,没有其他作用。相比下划线(如 std_core 或 std_regex),点的使用可以说提供了更好的可读性,下划线也是合法的,和其他一样组成了标识符。
模块有几个优点,特别跟头文件相比:
- 它们只导入一次且与导入的顺序无关;
- 它们不需要将接口和实现分离在不同的源文件,尽管这是可能的;
- 模块有减少编译时间的潜力,在某些情况下,可极大减少编译时间。模块导出的实体,在二进制文件中描述,相比传统预编译头文件,编译器可以处理得更快;
- 文件可用于构建和其他语言互操作的 C++ 代码。
在本节中,你将学习如何开始模块的使用。
C++模块的使用方式
当你模块化代码时,可以如下做:1) 使用 import 指令导入模块,紧跟着模块名。标准库尽管还没模块化,但可作为编译器特定模块存在。
以下代码片段使用了 Visual C++ 的 std.core 模块,它包含了标准库里大多数功能,包括流式库:
import std.core; int main() { std::cout << "Hello, World!\n"; }
2) 通过创建包含函数、类型、常量和宏的模块接口单元(Module Interface Unit,MIU)来导出模块。它们的声明必须以关键字 export 开头。
VC++ 模块接口单元文件的扩展必须是 .ixx;Clang 接受不同的扩展名,包括 .cpp、.cppm,甚至.ixx。
以下示例导出名为 point 的类模板、用于计算两点距离的 distance() 函数,以及用户自定义字面常量操作符 _ip,其从字符串形式"0,0"或"12,-3"中创建 point 类型的对象:
// --- geometry.ixx/.cppm --- export module geometry; import std.core; export template <class T, typename = typename std::enable_if_t<std::is_arithmetic_v<T>, T>> struct point { T x; T y; }; export using int_point = point<int>; export constexpr int_point int_point_zero{ 0,0 }; export template <class T> double distance(point<T> const& p1, point<T> const& p2) { return std::sqrt((p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y)); } namespace geometry_literals { export int_point operator ""_ip(const char* ptr, std::size_t size) { int x = 0, y = 0; while (*ptr != ',' && *ptr != ' ') x = x * 10 + (*ptr++ - '0'); while (*ptr == ',' || *ptr == ' ') ptr++; while (*ptr != 0) y = y * 10 + (*ptr++ - '0'); return { x, y }; } } // --- main.cpp --- import std.core; import geometry; int main() { int_point p{ 3, 4 }; std::cout << distance(int_point_zero, p) << '\n'; { using namespace geometry_literals; std::cout << distance("0,0"_ip, "30,40"_ip) << '\n'; } }
3) 使用 import 指令导入头文件内容。以下示例使用前一示例所示的同一类型和函数:
// --- geometry.h --- #pragma once #include <cmath> template <class T, typename = typename std::enable_if_t<std::is_arithmetic_v<T>, T>> struct point { T x; T y; }; using int_point = point<int>; constexpr int_point int_point_zero{ 0,0 }; template <class T> double distance(point<T> const& p1, point<T> const& p2) { return std::sqrt((p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y)); } namespace geometry_literals { int_point operator ""_ip(const char* ptr, std::size_t size) { int x = 0, y = 0; while (*ptr != ',' && *ptr != ' ') x = x * 10 + (*ptr++ - '0'); while (*ptr == ',' || *ptr == ' ') ptr++; while (*ptr != 0) y = y * 10 + (*ptr++ - '0'); return { x, y }; } } // --- main.cpp --- import std.core; import "geometry.h"; int main() { int_point p{ 3, 4 }; std::cout << distance(int_point_zero, p) << '\n'; { using namespace geometry_literals; std::cout << distance("0,0"_ip, "30,40"_ip) << '\n'; } }
深度剖析C++模块
模块单元由几个部分组成,强制或可选:- 由 module;语句引入的全局模块片段。这部分是可选的,如果存在,则可能只包含预处理器指令。所有添加到这里的都属于全局模块,是全局模块片段和所有不是模块的编译单元的合集。
- 模块声明,是必需的声明,形式为 export module name;。
- 模块前言,是可选的,只包含导入声明。
- 模块范围,是单元的内容,以模块声明开始,直到模块单元结束。
下图展示了模块包含之前提及的所有部分。在左边,我们有模块的源码;在右边,我们解释了模块部分。

图 1 模块(左边)示例,每部分高亮且被解释(右边)
模块可导出任何实体,如函数、类和常量。每次导出都以 export 关键字在前面。这个关键字通常是第一个关键字,在其他 class/struct、template 或 using 前。
前面的 geometry 模块中提供了几个示例:
- 类模板 point,表示二维空间里的点;
- int_point 是 point<int> 的别名;
- 编译时常量为 int_point_zero;
- 函数模板 distance(),计算两点间的距离;
- 用户自定义字面操作 _ip,从诸如"3,4"字符串中创建 int_point 对象。
使用模块而不使用头文件的编译单元,除了将 #include 预处理器指令替换为 import 指令外,不需要其他更改。此外,用同样的 import 指令,头文件也可如模块一样导入,就如之前示例所示。
模块和命名空间之间没有关系。这两个是正交的概念。模块 geometry 导出用户自定义字面""_ip到命名空间 geometry_literals,同时模块中其他的导出在全局命名空间可用。
模块名称和单元文件名称也没有关系。尽管任何文件名有同样的效果,geometry 模块在 geometry.ixx/.cppm 文件中定义。建议你遵循一致的命名规范,使用模块名作为模块文件名。另外,模块单元的扩展名因编译器而异,尽管当模块支持成熟后,将来可能会有变化。
标准库还没模块化,尽管这很可能在标准库未来版本中发生。然而,编译器已经将其模块化实现了。Clang 编译器为每个头文件提供了不同的模块。另外,Visual C++ 编译器为标准库提供了以下模块:
- std.regex:<regex> 头文件的内容;
- std.filesystem:<filesystem> 头文件的内容;
- std.memory:<memory> 头文件的内容;
- std.threading:头文件 <atomic>、<condition_variable>、<future>、<mutex>、<shared_mutex> 和 <thread> 的内容;
- std.core:C++ 标准库剩下头文件的内容。
如你从这些模块名(如 std.core 或 std.regex)中所见,模块名可以是一系列由点(.)拼接的标识符。点除了将名称分为代表逻辑层次部分外,如 company.project.module,没有其他作用。相比下划线(如 std_core 或 std_regex),点的使用可以说提供了更好的可读性,下划线也是合法的,和其他一样组成了标识符。