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),点的使用可以说提供了更好的可读性,下划线也是合法的,和其他一样组成了标识符。
ICP备案:
公安联网备案: