演练:导入 STL 库作为标头单位

此演练演示了如何在 Visual Studio 中导入 C++ 标准模板库 (STL) 库作为标头单元。 如需更快、更可靠地导入标准库的方法,请参阅教程:使用模块导入 C++ 标准库

导入 STL 标头作为标头单元比使用预编译标头文件更简单。 标头单元更易于设置和使用,在磁盘上明显较小,具有类似的性能优势,并且比共享 PCH 更灵活。

若要更详细地了解标头单元是什么及其提供的优势,请参阅什么是标头单元?。 若要将标头单元与其他标准库导入方法进行比较,请参阅比较标头单元、模块和预编译标头

先决条件

若要使用标头单元,请使用 Visual Studio 2022 或更高版本,或者 Visual Studio 2019 版本 16.11 或更高版本。 要使用标头单元,需要 /std:c++20 选项(或更高版本)。

导入 STL 标头作为标头单元的两种方法

导入 STL 标头之前,必须将其编译为标头单元。 标头单元是头文件的二进制表示形式。 它具有 .ifc 扩展名。

建议的方法是创建一个静态库,该库包含要使用的 STL 标头的生成标头单元。 然后引用该库并 import 其标头单元。 此方法可提高生成速度并更好地重复使用。 若要试用此方法,请参阅方法 1:创建 STL 库标头单元的静态库

另一种方法是让 Visual Studio 扫描你 #include 在项目中的 STL 标头,将它们编译为标头单元,然后 import 而不是 #include 这些标头。 如果你有大型代码库,此方法非常有用,因为不必更改源代码。 此方法不如静态库方法灵活,因为它本身无法重复使用其他项目中生成的标头单元。 但是,通过将单个 STL 库导入为标头单元,你仍然能够获得性能优势。 若要试用此方法,请参阅方法 2:扫描 includes 以查找要导入的 STL 标头

方法 1:创建 STL 库标头单元的静态库

要使用 STL 库作为标头单元,建议创建一个或多个静态库项目。 这些项目应包含你想要使用的 STL 库标头单元。 然后,引用库项目来使用这些 STL 标头单元。 这类似于使用共享预编译标头,但更简单。

静态库项目中生成的标头单元(和模块)会自动用于引用项目,因为项目系统会自动向编译器添加适当的 /headerUnit 命令行选项,以便引用项目可以导入标头单元。

此方法可确保仅生成特定标头的标头单元一次。 它允许导入某些或全部标头单元,这一点 PCH 无法做到。 可以按任意顺序包括标头单元。

在以下示例中,将创建一个包含 <iostream><vector> 标头单元的静态库项目。 生成解决方案后,将从另一个 C++ 项目中引用此共享标头单元项目。 无论在任何地方找到 import <iostream>;import <vector>;,都会使用该库的生成标头单元,而不是使用预处理器转换标头。 这可在多个文件中包含相同标头时提高生成性能,就像 PCH 文件一样。 不需要由包含该标头的文件对标头进行反复处理。 相反,将导入已处理的已编译标头单元。

若要创建包含 STL 库 <iostream><vector> 的静态库,请执行以下步骤:

  1. 创建一个空 C++ 项目。 将其命名为 SharedPrj。
    从“新建项目”窗口中可用的项目类型中,为 C++ 选择“空项目”:Screenshot that shows creating a new empty C++ project.

  2. 向项目添加一个新 C++ 文件。 将该文件的内容更改为:

    import <iostream>;
    import <vector>;
    

设置项目属性

设置项目属性以共享此项目中的标头单元:

  1. 在 Visual Studio 主菜单上,选择“项目”>“SharedPrj 属性”,打开项目的“属性页”对话框:Screenshot that shows settings for Configuration Type and C++ Language Standard.
  2. 在“配置”下拉列表中选择“所有配置”,然后在“平台”下拉列表中选择“所有平台”。 这些设置确保无论是你为调试还是发布构建的,都应用所做的更改。
  3. 在项目的“属性页”对话框的左侧窗格中,选择“配置属性”>“常规”。
  4. 将“配置类型”选项更改为“静态库(.lib)”
  5. 将“C++ 语言标准”更改为“ISO C++20 标准(/std:c++20)”(或更高版本)。
  6. 在项目的“属性页”对话框的左侧窗格中,选择“配置属性”>“C/C++”>“常规”。
  7. 将“在源中扫描模块依赖项”下拉列表中,选择“是”。 (此选项会导致编译器扫描代码中可内置到标头单元中的依赖项):Screenshot that shows the scan module dependencies property setting.
  8. 选择“确定”,关闭项目的“属性页”对话框。 通过在主菜单上选择“生成”>“生成解决方案”来生成解决方案

引用标题单元库

若要从静态库导入 <iostream><vector> 作为标头单元,请创建引用静态库的项目,如下所示:

  1. 在当前解决方案仍处于打开状态的情况下,从 Visual Studio 菜单中选择“文件”>“添加”>“新项目”

  2. 在“新建项目”向导中,选择 C++ 控制台应用模板,然后选择“下一步”。

  3. 将新项目命名为 Walkthrough。 将“解决方案”下拉列表更改为“添加到解决方案”。 选择“创建”来创建项目,并将其添加到解决方案中。

  4. 更改 Walkthrough.cpp 源文件的内容,如下所示:

    import <iostream>;
    import <vector>;
    
    int main()
    {
        std::vector<int> numbers = {0, 1, 2};
        std::cout << numbers[1];
    }
    

标头单元需要 /std:c++20 选项(或更高版本)。 使用以下步骤设置语言标准:

  1. 在“解决方案资源管理器”中,右键单击 Walkthrough 项目,然后选择“属性”来打开项目的“属性页”对话框:Screenshot that shows setting the language standard to the preview version.
  2. 在 Walkthrough 项目的“属性页”对话框的左侧窗格中,选择“配置属性”>“常规”。
  3. 在“C++ 语言标准”下拉列表中,选择“ISO C++20 标准(/std:c++20)”(或更高级别)。
  4. 选择“确定”,关闭项目的“属性页”对话框。

在“演练”项目中,使用以下步骤添加对 SharedPrj 项目的引用:

  1. 在“演练”项目中,选择“引用”节点,然后选择“添加引用”。 在项目列表中选择“SharedPrj”:Screenshot that shows the Add Reference dialog. It's used to add a reference to the Walkthrough project. 如果添加此引用,那么每当 Walkthrough 项目中的 import 与 SharedPrj 中已生成的标头单元之一匹配时,生成系统就会使用 SharedPrj 所生成的标头单元。
  2. 选择“确定”,关闭“添加引用”对话框。
  3. 右键单击“演练”项目,然后选择“设为启动项目”
  4. 生成解决方案。 (使用主菜单上的“生成”>“生成解决方案”。)运行它后,可以看到它生成了预期的输出:1

此方法的优点是,你可以从任何项目引用静态库项目,以重用其中的标头单元。 在此示例中,静态库包含 <vector><iostream> 标头单元。

你可以创建一个单一静态库项目,其中包含要从各个项目中导入的所有常用 STL 标头。 或者,可以为要导入为标头单元的 STL 库的不同分组创建较小的共享库项目。 然后,根据需要引用这些共享标头单元项目。

结果应增加生成吞吐量,因为导入标头单元可显著减少编译器必须执行的工作。

当你将此方法用于自己的项目时,可以使用编译器选项来生成静态库项目,这些选项与将引用该项目的项目兼容。 例如,应该使用 编译器选项来生成 STL 项目以打开异常处理,引用静态库项目的项目也应该如此操作/EHsc

使用 /translateInclude

通过 /translateInclude 编译器选项,可更轻松地在 #include STL 库的较旧项目中使用标头单元库(可在项目的“属性页”对话框中的“C/C++”>“常规”>“将 Include 转换为 Import”下找到此编译器选项)。 无需在项目中将 #include 指令更改为 import,同时仍可让你利用导入标头单元而不是包括它们的优势。

例如,如果你在项目中使用 #include <vector>,并且引用了包含 <vector> 的标头单元的静态库,则无需在源代码中手动将 #include <vector> 更改为 import <vector>;。 相反,编译器会将 #include <vector> 自动视为 import <vector>;。 有关此方法的更详细信息,请参阅方法 2:在 Include 中扫描要导入的 STL 标头。 并非所有 STL 头文件都可以编译为标头单元。 随 Visual Studio 提供的 header-units.json 列出了哪些 STL 头文件可以编译为标头单元。 依赖宏来指定其行为的标头通常无法编译为标头单元。

未引用标头单元的 #include 语句被视为普通 #include

在项目间重复使用标头单元

由静态库项目生成的标头单元可自动用于所有直接和间接引用的项目。 有一些项目设置使你可以选择哪些标头单元应自动用于所有引用的项目。 这些设置位于“VC++ 目录”下的项目设置中

  1. 在“解决方案资源管理器”中,右键单击项目,然后选择“属性”来打开项目的“属性页”对话框。
  2. 在对话框的左侧窗格中,选择“配置属性”“VC++ 目录”:>Screenshot that shows public project content properties, like Public Include Directories and All Header Files are Public.

以下属性控制标头单元对生成系统的可见性:

  • 公共 Include 目录:指定应自动添加到引用项目中的 include 路径的标头单元的项目目录。
  • 公共 C++ 模块目录:指定哪些项目目录包含可用于引用项目的标头单元。 通过此属性,可你将一些标头单元设为“公用”。 它对其他项目可见,因此可在其中放置要共享的标头单元。 如果使用此设置,为方便起见,请指定“公共 Include 目录”,将公共标头自动添加到引用项目中的 include 路径。
  • 所有模块都是公共的:使用作为 DLL 项目的一部分生成的标头单元时,必须从 DLL 中导出符号。 若要自动导出模块符号,请将此属性设置为“是”。

使用预生成模块文件

通常,在解决方案之间重复使用标头单元的最简单方法是从每个解决方案中引用共享标头单元项目。

如果必须使用没有项目的内置标头单元,则可以指定生成的 .ifc 文件的位置,以便将其导入解决方案。 若要访问此设置,请执行以下操作:

  1. 在主菜单上,选择“项目”>“属性”,打开项目的“属性页”对话框。
  2. 在对话框的左侧窗格中,选择“配置属性”>“C/C++”:“常规”。
  3. 在“其他模块依赖项”列表中,添加要引用的模块(用分号分隔)。 下面的示例显示了用于其他模块依赖项的格式:ModuleName1=Path\To\ModuleName1.ifc; ModuleName2=Path\To\ModuleName2.ifcScreenshot showing project Property Pages properties under Configuration Properties, C/C++, General, with Additional Module Dependencies selected.

在标头单元的多个副本中进行选择

如果引用生成多个标头单元的项目(无论是具有相同名称还是用于同一头文件),必须指定要使用哪一个项目。 例如,你可能有使用不同编译器设置生成的不同版本的标头单元,并且必须指定与项目设置匹配的版本。

使用项目的“其他标头单元依赖项”属性,通过指定要使用哪个标头单元来解决冲突。 否则,无法预测选取哪一个。

若要设置“其他标头单元依赖项”属性,请执行以下操作:

  1. 在主菜单上,选择“项目”>“属性”,打开项目的“属性页”对话框。
  2. 在对话框的左侧窗格中,选择“配置属性”>“C/C++”:“常规”。
  3. 要解决冲突,请指定要在“其他标头单元依赖项”中使用哪些模块或标头单元文件。 将此格式用于其他标头单元依赖项:Path\To\Header1.h= Path\To\HeaderUnit1.ifc;Path\To\Header2.h= Path\To\ HeaderUnit2.ifcScreenshot that shows the Additional Header Unit Dependencies setting in the project Property Pages dialog.

重要

确保使用兼容的编译选项生成共享标头单元的项目。 如果实现标头单元时使用的编译选项不同于创建标头单元时使用的编译选项,则编译器将发出警告。

注意

要使用 DLL 项目中生成的标头单元,请将“所有模块都是公共的”设置为“是”

方法 2:扫描 includes 以查找要导入的 STL 标头

导入 STL 库的另一种方法是让 Visual Studio 扫描 #include 在项目中的 STL 标头,并将其编译为标头单元。 然后,编译器将导入而不是包括这些标头。

当项目包含多个文件中的多个 STL 头文件或生成吞吐量不重要时,此选项很方便。 此选项不能保证特定标头文件的标头单元仅生成一次。 但是,如果你有大型代码库,这将非常有用:你不必更改源代码,就可利用你使用的许多 STL 库的标头单元的优势。

此方法不如静态库方法灵活,因为它本身无法重复使用其他项目中的生成标头单元。 此方法可能不适合大型项目:它不能保证最佳生成时间,因为必须扫描所有源来查找 #include 语句。

并非所有标头文件都可以自动转换为标头单元。 例如,不应将通过宏依赖于条件编译的标头转换为标头单元。 编译器在指定 header-units.json 时使用的 STL 标头有一个 /translateInclude 文件形式的允许列表。 它将确定哪些 STL 标头可以编译为标头单元。 header-units.json 文件位于 Visual Studio 的安装目录下。 例如 %ProgramFiles%\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.30.30705\include\header-units.json。 如果 STL 头文件不在列表中,则将其视为普通 #include,而不是将其作为标头单元导入。 header-units.json 文件的另一个优点是,它可以防止生成的标头单元中的符号出现重复。 也就是说,如果编译标头单元多次引入另一个库标头,则符号不会重复。

若要试用此方法,请创建一个包含两个 STL 库的项目。 然后更改项目属性,以便将库作为标头单元导入,而不是包括它们,如下一部分所述。

创建 C++ 控制台应用项目

按照以下步骤创建包含两个 STL 库的项目:<iostream><vector>

  1. 在 Visual Studio 中,创建新的 C++ 控制台应用项目。

  2. 替换源文件的内容,如下所示:

    #include <iostream>;
    #include <vector>;
    
    int main()
    {
        std::vector<int> numbers = {0, 1, 2};
        std::cout << numbers[1];
    }
    

设置项目选项并运行项目

以下步骤将设置选项,使编译器扫描包括的标头以转换为标头单元。 它们还将设置选项,使编译器将 #include 视为你已经为可以视为标头单元的标头文件编写了 import

  1. 在主菜单上,选择“项目”>“属性”,打开项目的“属性页”对话框。
  2. 在“配置”下拉列表中选择“所有配置”,然后在“平台”下拉列表中选择“所有平台”。 这些设置确保无论是你为调试还是发布构建的,都应用所做的更改,还应用其他配置。
  3. 在对话框的左侧窗格中,选择“配置属性”>“C/C++”:“常规”。
  4. 将“扫描源以查找模块依赖项”设置为“是”。 此设置可确保所有兼容的头文件都编译为标头单元。
  5. 将“将 Include 转换为 Import”设置为“是”。 此设置将 header-unit.json 文件中列出的 STL 头文件编译为标头单元,然后将其导入,而不是使用预处理器对它们进行 #include 处理。 Screenshot that shows the scan module dependencies property setting in the project Property Pages.
  6. 选择“确定”来保存更改,并返回到项目的“属性页”对话框。

要使用标头单元,需要 /std:c++20 选项或更高版本。 若要更改编译器使用的 C++ 语言标准,请执行以下操作:

  1. 在主菜单上,选择“项目”>“属性”,打开项目的“属性页”对话框。
  2. 在“配置”下拉列表中选择“所有配置”,然后在“平台”下拉列表中选择“所有平台”。 这些设置确保无论是你为调试还是发布构建的,都应用所做的更改,还应用其他配置。
  3. 在项目的“属性页”对话框的左侧窗格中,选择“配置属性”>“常规”。
  4. 在“C++ 语言标准”下拉列表中,选择“ISO C++20 Standard (/std:c++20)”(或更高级别)。
  5. 选择“确定”来保存更改,并返回到项目的“属性页”对话框。
  6. 在主菜单中,通过选择“生成”>“生成解决方案”来生成解决方案

可以运行解决方案以验证它是否能够生成预期的输出:1

是否使用此方法的主要考虑因素在于以下两个方面之间的平衡性,即方便性与扫描所有文件以确定要生成为标头单元的头文件的生成成本。

另请参阅

比较标头单元、模块和预编译标头
教程:使用模块导入 C++ 标准库
演练:在 Visual C++ 项目中生成和导入标头单元
/translateInclude