本文介绍如何使用 Visual Studio 2022 生成和导入标头单元。 若要了解如何将 C++ 标准库标头导入为标头单元,请参阅
演练:将 STL 库导入为标头单元
。 有关导入标准库的更快、更可靠的方法,请参阅
教程:使用模块导入 C++ 标准库
。
标头单元是
预编译头文件
(PCH) 的推荐替代方法。 标头单元更易于设置和使用,在磁盘上明显较小,具有类似的性能优势,并且比
共享 PCH
更灵活。
若要将标头单元与其他在程序中包括功能的方法进行比较,请参阅
比较标头单元、模块和预编译标头
。
若要使用标头单元,需要使用 Visual Studio 2019 16.10 或更高版本。
标头单元是头文件的二进制表示形式。 标头单元以
.ifc
扩展名结尾。 相同的格式也用于命名模块。
标头单元和头文件之间的一个重要区别是,标头单元不受标头单元之外的宏定义的影响。 也就是说,无法定义导致标头单元行为不同的预处理器符号。 当你导入标头单元时,就已经编译了标头单元。 这与文件的处理方式
#include
不同。 包含的文件可能受头文件外部的宏定义的影响,因为当你编译包含它的源文件时,头文件会经过预处理器。
标头单元可以按任何顺序导入,而头文件则不是如此。 头文件顺序很重要,因为在一个头文件中定义的宏定义可能会影响后续头文件。 一个标头单元中的宏定义不会影响另一个标头单元。
头文件中可见的所有内容在标头单元中也都可见,其中包括标头单元内定义的宏。
必须将头文件转换为标头单元,才能导入该头文件。 与预编译头文件 (PCH) ,标头单元的优点是可以在分布式生成中使用。 只要使用相同的编译器编译
.ifc
和导入它的程序,并且以相同的平台和体系结构为目标,就可以在另一台计算机上使用在一台计算机上生成的标头单元。 与 PCH 不同,当标头单元发生更改时,仅重新生成它及其所依赖的内容。 标头单元的大小最多可以比 小一个
.pch
数量级。
与 PCH 相比,标头单元对用于创建标头单元和编译使用标头单元的代码的编译器开关组合所需的相似性施加的约束更少。 但是,某些开关组合和宏定义可能会在不同转换单元之间 (ODR) 造成单一定义规则的冲突。
最后,标头单元比 PCH 更灵活。 使用 PCH 时,不能选择只引入 PCH 中的一个标头,编译器会处理所有这些标头。 使用标头单元时,即使将它们一起编译到一个静态库中,也只会引入你导入到应用程序中的标头单元的内容。
标头单元是头文件和 C++ 20 模块之间的一个步骤。 它们提供了模块的一些优势。 它们更可靠,因为外部宏定义不会影响它们,因此可以按任何顺序导入它们。 编译器可以比头文件更快地处理它们。 但标头单元没有模块的所有优点,因为标头单元公开在其中定义的宏, (模块不) 。 与模块不同,无法在标头单元中隐藏专用实现。 为了表示头文件的专用实现,我们采用了不同的技术,例如向名称添加前导下划线,或将内容放入实现命名空间中。 模块不会以任何形式公开专用实现,因此无需执行此操作。
请考虑将预编译标头替换为标头单元。 可以获得相同的速度优势,但也有其他代码的卫生和灵活性优势。
可以通过多种方法将文件编译为标头单元:
生成共享标头单元项目
。 建议使用此方法,因为它可以更好地控制组织和重用导入的标头单元。 创建包含所需标头单元的静态库项目,然后引用该项目以导入标头单元。 有关此方法的演练,请参阅
为标头单元生成标头单元静态库项目
。
选择要转换为标头单元的各个文件。 使用此方法可以逐个文件控制将哪些文件视为标头单元。 如果必须将文件编译为标头单元,这也很有用,因为它没有默认扩展名(
.ixx
、
.cppm
、
.h
、
.hpp
),通常不会被编译为标头单元。 本演练中介绍了这种方法。 若要开始,请参阅
方法 1:将特定文件转换为标头单元
。
自动扫描和生成标头单元。 此方法很方便,但最适用于较小的项目,因为它不能保证最佳的生成吞吐量。 有关此方法的详细信息,请参阅
方法 2:自动扫描标头单元
。
如简介中所述,可以生成 STL 头文件并将其导入为标头单元,并自动将 STL 库标头视为
#include
import
,而无需重写代码。 若要了解如何操作,请访问
演练:将 STL 库作为标头单元导入
。
方法 1:将特定文件转换为标头单元
本部分介绍如何选择要转换为标头单元的具体文件。 在 Visual Studio 中使用以下步骤将头文件编译为标头单元:
新建 C++ 控制台应用项目。
按如下所示替换源文件内容:
#include "Pythagorean.h"
int main()
PrintPythagoreanTriple(2,3);
return 0;
添加一个名为 Pythagorean.h
的头文件,然后将其内容替换为以下代码:
#ifndef PYTHAGOREAN
#define PYTHAGOREAN
#include <iostream>
inline void PrintPythagoreanTriple(int a, int b)
std::cout << "Pythagorean triple a:" << a << " b:" << b << " c:" << a*a + b*b << std::endl;
#endif
设置项目属性
若要启用标头单元,请首先使用以下步骤将 C++ 语言标准 设置为 /std:c++20
或更高版本:
在“解决方案资源管理器”中,右键单击项目名称,然后选择“属性”。
在项目属性页窗口的左侧窗格中,选择“配置属性”>“常规” 。
在“C++ 语言标准”下拉列表中,选择“ISO C++20 Standard (/std:c++20)”或更高级别。 选择“确定”以关闭对话框。
将头文件编译为标头单元:
在“解决方案资源管理器”中,选择要编译为标头单元的文件(在本例中为 Pythagorean.h
)。 右键单击该文件,然后选择“属性”。
在“配置属性”>“常规”>“项类型”下拉列表中设置“C/C++ 编译器”,然后选择“确定”。
在本演练的后面部分生成此项目时, Pythagorean.h
将转换为标头单元。 它被转换为标头单元,因为此头文件的项类型设置为 C/C++ 编译器,并且由于 以这种方式设置 和 .hpp
文件的默认操作.h
是将文件转换为标头单元。
这不是本演练所必需的,但可以供你参考。 若要将文件编译为没有默认标头单元文件扩展名的标头单元(例如 .cpp
),请在“配置属性”>“C/C++”>“高级”>“编译为”中设置“编译为 C++ 标头单元(/exportHeader)”:
在示例项目的源文件中,更改为#include "Pythagorean.h"
import "Pythagorean.h";
“不要忘记尾随分号”。 它对于 语句是必需的 import
。 由于它是项目本地目录中的头文件,因此我们在 语句中使用 import
引号: import "file";
。 在你自己的项目中,若要从系统标头编译标头单元,请使用尖括号: import <file>;
通过在主菜单上选择“生成”>“生成解决方案”来生成解决方案 。 运行它后,可以看到它生成了预期的输出:Pythagorean triple a:2 b:3 c:13
在你自己的项目中,重复此过程以编译要作为标头单元导入的头文件。
如果你只想将一些头文件转换为标头单元,这是一种很好的方法。 但是,如果要编译许多头文件,并且生成系统自动处理这些文件的便利性超过了生成性能的潜在损失,请参阅以下部分。
如果你有兴趣专门将 STL 库标头作为标头单元导入,请参阅演练:将 STL 库作为标头单元导入。
方法 2:自动扫描和生成标头单元
由于扫描所有源文件的头单元和生成它们需要时间,因此以下方法最适合较小的项目。 它不能保证最佳的生成吞吐量。
此方法合并了两个 Visual Studio 项目设置:
“扫描源以查找模块依赖项”会导致生成系统调用编译器,以确保在编译依赖它们的文件之前生成所有导入的模块和标头单元。 与 “将包含内容转换为导入”结合使用时,源中包含的任何在与头文件位于同一 header-units.json
目录中的文件中指定的头文件都编译为标头单元。
如果 #include
引用可编译为标头单元的头文件(如 header-units.json
文件中所指定的),并且已编译的标头单元可用于头文件,“将包含转换为导入”会将头文件视为 import
。 否则,头文件被视为普通 #include
。 文件 header-units.json
用于为每个 #include
自动生成标头单元,而无需重复符号。
可以在项目的属性中启用这些设置。 为此,请在“解决方案资源管理器”中右键单击项目,然后选择“属性”。 然后选择“配置属性”>“C/C++”>“常规”。
可以为“项目属性”的项目中的所有文件设置“扫描源以查找模块依赖项”,如图所示,也可以为“文件属性”中的个别文件设置。 始终会扫描模块和标头单元。 当你有一个 .cpp
文件导入了你希望自动生成但可能还没有生成的标头单元时,可以设置此选项。
这些设置共同发挥作用,在以下条件下自动生成和导入标头单元:
扫描模块依赖项的源 会扫描源,查找可被视为标头单元的文件及其依赖项。 无论此设置如何,都会始终扫描扩展名.ixx
为 的文件以及其 File 属性>C/C++>Compile As 属性设置为 Compile as C++ Header Unit (/export) 的文件。 编译器还会查找 import
语句以标识标头单元依赖项。 如果已指定 /translateInclude
,编译器还会扫描同时在 header-units.json
文件中指定的 #include
指令,以将其视为标头单元。 依赖项图由项目中的所有模块和标头单元生成。
“将包含转换为导入”,当编译器遇到 #include
语句,并且指定的头文件存在匹配的标头单元文件 (.ifc
) 时,编译器会导入标头单元,而不是将头文件视为 #include
。 与“扫描依赖项”结合使用时,编译器会查找可编译为标头单元的所有头文件。 编译器会参考允许列表,以确定哪些头文件可以被编译为标头单元。 此列表存储在 header-units.json
文件中,此文件必须与包含的文件位于同一目录中。 可以在 Visual Studio 的安装目录下看到 header-units.json
文件的示例。 例如,编译器使用 %ProgramFiles%\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.30.30705\include\header-units.json
来确定是否可以将标准模板库标头编译为标头单元。 此功能作为与旧代码之间的桥梁,以获得标头单元的一些优势。
header-units.json
文件有两种用途。 除了指定哪些头文件可以被编译为标头单元之外,还可以最大程度地减少重复的符号以提高生成吞吐量。 有关符号重复的详细信息,请参阅 C++ header-units.json 参考。
这些开关和 header-unit.json
提供了标头单元的一些优势。 获得便利的代价是产生了吞吐量。 对于较大的项目,此方法可能不是最佳的方法,因为它不能保证最佳的生成时间。 此外,相同的头文件可能会重复重新处理,这会增加生成时间。 但是,根据项目的情况,这种便利可能是值得的。
这些功能专为旧代码而设计。 对于新代码,请移至模块,而不是标头单元或 #include
文件。 有关使用模块的教程,请参阅名称模块教程 (C++)。
有关如何使用此方法将 STL 头文件作为标头单元导入的示例,请参阅演练:将 STL 库作为标头单元导入。
预处理器的影响
创建和使用标头单元需要符合标准 C99/C++11 的预处理器。 编译器在编译标头单元时,无论使用哪种形式的 /exportHeader
,都会在命令行中隐式添加 /Zc:preprocessor
,从而启用新的符合 C99/C++11 的预处理器。 尝试将其禁用将导致编译错误。
启用新的预处理器会影响可变参数宏的处理。 有关详细信息,请参阅可变参数宏的备注部分。
/translateInclude
/exportHeader
/headerUnit
header-units.json
比较标头单元、模块和预编译标头
C++ 中的模块概述
教程:使用模块导入 C++ 标准库
演练:导入 STL 库作为标头单位