C++ 中的模块概述
C++20 引入了模块。 模块是一组源代码文件,这些文件独立于源文件(或者更确切地说,导入它们的转换单元 进行编译。
模块可消除或减少与使用头文件相关的许多问题。 它们通常会减少编译时间,有时甚至会显著减少。 在模块中声明的宏、预处理器指令和非导出名称在模块外部是不可见的。 它们不会影响导入模块的转换单元的编译。 你可以按任何顺序导入模块,而无需考虑宏重新定义。 导入转换单元中的声明不参与导入模块中的重载解析或名称查找。 编译一次模块后,结果将存储在描述所有导出的类型、函数和模板的二进制文件中。 编译器可以比头文件更快地处理该文件。 而且,编译器可以在项目中导入模块的每个位置重复使用该文件。
可以将模块与头文件并行使用。 C++ 源文件可以 import
模块,并同时 #include
头文件。 在一些情况下,可以将头文件导入为模块,这比通过预处理器使用#include
处理它更快。 建议在新项目中尽可能多地使用模块,而不是头文件。 对于正在开发中的大型现有项目,请尝试将旧标头转换为模块。 根据能否显著缩短编译时间来确定是否采用。
要将模块与其他导入标准库的方法进行比较,请参阅比较头单元、模块和预编译标头。
在 Microsoft C++ 编译器中启用模块
从 Visual Studio 2022 版本 17.1 起,C++20 标准模块已在 Microsoft C++ 编译器中完全实现。
在 C++20 标准指定之前,Microsoft 对模块具有实验性支持。 编译器还支持导入预生成的标准库模块,如下所述。
从 Visual Studio 2022 版本 17.5 开始,将标准库作为模块导入在 Microsoft C++ 编译器中是标准化且完全实现的操作。 本部分介绍了仍受支持的较旧的实验方法。 有关使用模块导入标准库的新标准化方法的信息,请参阅使用模块导入 C++ 标准库。
可以使用模块功能创建单分区模块并导入 Microsoft 提供的标准库模块。 若要启用对标准库模块的支持,请使用 /experimental:module
和 /std:c++latest
。 在 Visual Studio 项目中,右键单击“解决方案资源管理器”中的项目节点,然后选择“属性”。 将“配置”下拉列表设置为“所有配置”,然后选择“配置属性”>“C/C++”>“语言”>“启用 C++ 模块(实验性)”。
必须使用相同的编译器选项编译使用它的模块和代码。
使用 C++ 标准库作为模块(实验性)
本部分介绍了仍受支持的实验性实现。 使用 C++ 标准库的新标准化方式如使用模块导入 C++ 标准库中所述。
通过将 C++ 标准库作为模块导入(而不是通过头文件包含它),可以根据项目的规模加快编译时间。 实验性库拆分为以下命名模块:
std.regex
提供标头<regex>
的内容std.filesystem
提供标头<filesystem>
的内容std.memory
提供标头<memory>
的内容std.threading
提供标头<atomic>
、<condition_variable>
、<future>
、<mutex>
、<shared_mutex>
和<thread>
的内容std.core
提供 C++ 标准库中的任何其他内容
若要使用这些模块,请将导入声明添加到源代码文件的顶部。 例如:
import std.core;
import std.regex;
若要使用 Microsoft 标准库模块,请使用 /EHsc
和 /MD
选项编译程序。
示例
以下示例显示了名为 Example.ixx
的源文件中的简单模块定义。 Visual Studio 中的模块接口文件需要 .ixx
扩展。 在此示例中,接口文件同时包含函数定义和声明。 但是,还可以将定义放置在一个或多个单独的模块实现文件中,如后面的示例所示。
export module Example;
语句指示此文件是名为 Example
的模块的主接口。 f()
上的 export
修饰符指示此函数在另一个程序或模块导入 Example
时可见。
// Example.ixx
export module Example;
#define ANSWER 42
namespace Example_NS
{
int f_internal() {
return ANSWER;
}
export int f() {
return f_internal();
}
}
该文件 MyProgram.cpp
使用 import
访问由 Example
导出的名称。 命名空间名称 Example_NS
在此处可见,但不是所有成员均可见,因为它们未导出。 此外,宏 ANSWER
不可见,因为宏未导出。
// MyProgram.cpp
import Example;
import std.core;
using namespace std;
int main()
{
cout << "The result of f() is " << Example_NS::f() << endl; // 42
// int i = Example_NS::f_internal(); // C2039
// int j = ANSWER; //C2065
}
import
声明仅在全局范围内显示。
模块语法
module-name
?
module-name-qualifier-seq
optidentifier
module-name-qualifier-seq
?
identifier
.
module-name-qualifier-seq
identifier
.
module-partition
?
:
module-name
module-declaration
?
export
optmodule
module-name
module-partition
optattribute-specifier-seq
opt;
module-import-declaration
?
export
optimport
module-name
attribute-specifier-seq
opt;
export
optimport
module-partition
attribute-specifier-seq
opt;
export
optimport
header-name
attribute-specifier-seq
opt;
实现模块
模块接口导出模块名称以及构成模块的公共接口的所有命名空间、类型和函数等。
模块实现定义模块导出的内容。
模块的最简单形式可以是一个结合了模块接口和实现的文件。 还可以将实现放入一个或多个单独的模块实现文件中,类似于 .h
和 .cpp
文件的操作方式。
对于较大的模块,可以将模块的各个部分拆分为称为“分区”子模块。 每个分区由导出模块分区名称的模块接口文件组成。 分区可能还具有一个或多个分区实现文件。 整个模块有一个主模块接口,它是模块的公共接口。 如果需要,它可以导出分区接口。
模块由一个或多个模块单元组成。 模块单元是一个包含模块声明的转换单元(源文件)。 有多种类型的模块单元:
- 模块接口单元会导出模块名称或模块分区名称。 模块接口单元在其模块声明中具有
export module
。 - 模块实现单元不会导出模块名称或模块分区名称。 顾名思义,它可实现模块。
- 主模块接口单元会导出模块名称。 一个模块中必须且只能有一个主模块接口单元。
- 模块分区接口单元会导出模块分区名称。
- 模块分区实现单元在其模块声明中具有模块分区名称,但没有
export
关键字。
export
关键字仅用于接口文件。 实现文件可以 import
另一个模块,但不能 export
任何名称。 实现文件可以具有任何扩展。
模块、命名空间和依赖于自变量的查找
模块中命名空间的规则与任何其他代码中的规则相同。 如果导出命名空间中的声明,则也会隐式导出封闭命名空间(不包括未在该命名空间中显式导出的成员)。 如果显式导出命名空间,则会导出该命名空间定义中的所有声明。
当编译器在导入转换单元中为重载解析执行依赖于自变量的查找时,它会考虑使用在定义函数参数类型的同一转换单元(包括模块接口)中声明的函数。
模块分区
模块分区类似于模块,但以下情况除外:
- 它共享整个模块中所有声明的所有权。
- 分区接口文件导出的所有名称都必须由主接口文件导入并导出。
- 分区的名称必须以模块名称开头,后跟冒号 (
:
)。 - 任何分区中的声明在整个模块中都可见。\
- 无需采取特殊预防措施来避免单定义规则 (ODR) 错误。 可以在一个分区中声明名称(函数、类等),并在另一个分区中定义它。
分区实现文件从 C++ 标准的角度开始如下,是内部分区:
module Example:part1;
分区接口文件的开头如下所示:
export module Example:part1;
若要访问另一个分区中的声明,分区必须导入它。 但它只能使用分区名称,而不能使用模块名称:
module Example:part2;
import :part1;
主接口单元必须导入并重新导出模块的所有接口分区文件,如下所示:
export import :part1;
export import :part2;
主接口单元可以导入分区实现文件,但无法导出它们。 不允许这些文件导出任何名称。 这一限制使模块能够在模块内部保留实现详细信息。
模块和头文件
可以通过在模块声明前放置 #include
指令,将头文件包含在模块源文件中。 这些文件被视为位于全局模块片段中。 模块只能查看其显式包含的标头中的全局模块片段中的名称。 全局模块片段仅包含使用的符号。
// MyModuleA.cpp
#include "customlib.h"
#include "anotherlib.h"
import std.core;
import MyModuleB;
//... rest of file
可以使用传统的头文件来控制导入的模块:
// MyProgram.h
import std.core;
#ifdef DEBUG_LOGGING
import std.filesystem;
#endif
导入的头文件
某些标头已足够自包含,可以使用 import
关键字将其引入。 导入的标头与导入的模块之间的主要区别在于,标头中的任何预处理器定义在 import
语句后立即在导入程序中可见。
import <vector>;
import "myheader.h";
另请参阅
.- .
命名模块教程
比较标头单元、模块和预编译标头