教程:使用命令行中的模块导入C++标准库

了解如何使用C++库模块导入C++标准库。 这可以加快编译速度,比使用头文件、标头单元或预编译标头(PCH)更可靠。

在本教程中,了解以下内容:

  • 如何从命令行将标准库作为模块导入。
  • 模块的性能和可用性优势。
  • 这两个标准库模块 std 及其 std.compat 之间的差异。

先决条件

本教程需要 Visual Studio 2022 17.5 或更高版本。

标准库模块简介

头文件语义可能会根据宏定义、包含它们的顺序以及编译速度缓慢而更改。 模块解决这些问题。

现在可以将标准库作为模块导入,而不是作为一堆头文件导入。 这比包括头文件、标头单元或预编译标头(PCH)更快、更可靠。

C++23 标准库引入了两个命名模块:stdstd.compat

  • std 导出C++标准库命名空间 std中定义的声明和名称,例如 std::vector。 它还会导出 C 包装器标头的内容,这些<cstdio><cstdlib>标头提供类似std::printf()函数的内容。 全局命名空间中定义的 C 函数(例如 ::printf())不会导出。 这改进了包括 C 包装器头文件(如 )时也包含 C 头文件 的情况,这些头文件将引入 C 全局命名空间版本。 如果导入 std,则这不是问题。
  • std.compat导出所有内容std,并添加 C 运行时全局命名空间,例如::printf::fopen::size_t::strlen,等等。 通过该 std.compat 模块,可以更轻松地使用引用全局命名空间中的许多 C 运行时函数/类型的代码库。

编译器在使用 import std;import std.compat; 时导入整个标准库,并且这个过程比引入单个头文件更快。 例如,使用import std;(或import std.compat)引入整个标准库的速度比#include <vector>要快。

由于命名模块不会公开宏,因此当您导入stdstd.compat时,诸如offsetofva_arg和其他宏都不可用。 有关解决方法,请参阅 名为标准库的模块注意事项

关于C++模块

头文件用于在C++中的源文件之间共享声明和定义。 在标准库模块之前,你会通过指令(例如 #include <vector>)引入所需部分的标准库。 头文件很脆弱且难以撰写,因为它们的语义可能会根据你包括的顺序或是否定义了某些宏而更改。 它们也缓慢编译,因为它们由包含它们的每个源文件重新处理。

C++20 引入了一个名为 模块的新式替代方法。 在 C++23 中,我们能够利用模块支持来引入命名模块来表示标准库。

与头文件一样,模块允许跨源文件共享声明和定义。 但与头文件不同,模块并不脆弱,并且更易于撰写,因为它们的语义不会因宏定义或导入它们的顺序而更改。 编译器处理模块的速度比处理 #include 文件快得多,在编译时也使用更少的内存。 命名模块不公开宏定义或专用实现详细信息。

本文演示了使用标准库的新增和最佳方法。 有关使用标准库的替代方法的详细信息,请参阅 比较标头单元、模块和预编译标头

使用 std 导入标准库

以下示例演示如何使用命令行编译器将标准库用作模块。 有关如何在 Visual Studio IDE 中执行此作的信息,请参阅 生成 ISO C++23 标准库模块

该语句 import std;import std.compat; 将标准库导入应用程序。 但首先,必须将命名模块的标准库编译为二进制形式。 以下步骤演示如何操作。

示例:如何生成和导入 std

  1. 打开适用于 VS 的 x86 Native Tools 命令提示符:从 Windows 开始 菜单中,键入 x86 本机 ,提示应显示在应用列表中。 确保提示为 Visual Studio 2022 版本 17.5 或更高版本。 如果使用错误的提示版本,则会收到错误。 本教程中使用的示例适用于 CMD shell。

  2. 创建目录,例如 %USERPROFILE%\source\repos\STLModules,并将其设为当前目录。 如果选择没有写入访问权限的目录,则编译过程中会出现错误。

  3. 用以下命令编译名为std的模块:

    cl /std:c++latest /EHsc /nologo /W4 /c "%VCToolsInstallDir%\modules\std.ixx"
    

    如果收到错误,请确保使用正确的命令提示符版本。

    使用与导入该生成模块的代码相同的编译器设置来编译std命名的模块。 如果您有一个多项目解决方案,可以编译名为 module 的标准库,然后使用编译器选项 /reference 在所有项目中引用它。

    使用前面的编译器命令,编译器输出两个文件:

    • std.ifc 是编译器查阅处理 import std; 语句的命名模块接口的已编译二进制表示形式。 这是仅限编译时使用的工件。 它不会随应用程序一起交付。
    • std.obj 包含命名模块的实现。 编译示例应用以静态方式将标准库中使用的功能链接到应用程序时,请 std.obj 添加到命令行。

    此示例中的关键命令行开关包括:

    开关 Meaning
    /std:c++latest 使用最新版本C++语言标准和库。 虽然在/std:c++20版本下提供了模块支持,但需要最新的标准库才能获取对标准库命名模块的支持。
    /EHsc 使用C++异常处理,但标记 extern "C"的函数除外。
    /W4 通常建议使用 /W4 ,尤其是对于新项目,因为它启用所有级别 1、级别 2、级别 3 和大多数级别 4(信息性)警告,这有助于提前捕获潜在问题。 它实质上提供了类似于 lint 的警告,可帮助确保尽可能少的难以查找的代码缺陷。
    /c 编译时不进行链接,因为我们此时只是生成名为二进制的模块接口。

    可以使用以下开关控制对象文件名和命名模块接口文件名:

    • /Fo 设置对象文件的名称。 例如,/Fo:"somethingelse"。 默认情况下,编译器使用与要编译的模块源文件(.ixx)相同的对象文件的名称。 在此示例中,对象文件名 std.obj 默认为,因为我们正在编译模块文件 std.ixx
    • /ifcOutput 设置命名模块接口文件的名称(.ifc)。 例如,/ifcOutput "somethingelse.ifc"。 默认情况下,编译器将模块接口文件 (.ifc) 的名称与编译的模块源文件 (.ixx) 使用相同的名称。 在此示例中,生成的 ifc 文件 std.ifc 默认为因为我们正在编译模块文件 std.ixx
  4. 首先创建一个名为importExample.cpp的文件,文件中包含以下内容,然后导入您构建的std库。

    // requires /std:c++latest
    
    import std;
    
    int main()
    {
        std::cout << "Import the STL library for best performance\n";
        std::vector<int> v{5, 5, 5};
        for (const auto& e : v)
        {
            std::cout << e;
        }
    }
    

    在前面的代码中, import std; 替换 #include <vector>#include <iostream>。 该语句 import std; 使所有标准库都可用于一个语句。 导入整个标准库通常比处理单个标准库头文件快得多,例如 #include <vector>

  5. 使用与上一步相同的目录中的以下命令编译示例:

    cl /c /std:c++latest /EHsc /nologo /W4 /reference "std=std.ifc" importExample.cpp
    link importExample.obj std.obj
    

    不需要在此示例中的命令行上指定 /reference "std=std.ifc" ,因为编译器会自动查找 .ifc 与语句指定的 import 模块名称匹配的文件。 当编译器遇到 import std; 时,如果它位于与源代码相同的目录中,编译器可以找到 std.ifc.ifc如果文件位于与源代码不同的目录中,请使用/reference编译器开关来引用它。

    在此示例中,编译源代码并将模块的实现链接到应用程序是单独的步骤。 他们不必非得这样。 你可以使用 cl /std:c++latest /EHsc /nologo /W4 /reference "std=std.ifc" importExample.cpp std.obj 在一步中进行编译和链接。 但是,单独生成和链接可能很方便,因为在生成的链接步骤中,只需生成一次名为模块的标准库,然后就可以从项目或多个项目引用它。

    如果您构建单个项目,可以通过将 "%VCToolsInstallDir%\modules\std.ixx" 添加到命令行来同时执行构建 std 标准库模块的步骤和构建您的应用程序的步骤。 将其置于使用.cpp模块的任何std文件之前。

    默认情况下,输出可执行文件的名称取自第一个输入文件。 /Fe使用编译器选项指定所需的可执行文件名称。 本教程演示如何将 std 命名模块单独编译为步骤,因为标准库的命名模块只需编译一次,然后便可在您的项目或多个项目中引用。 但是,将所有东西一起构建可能很方便,如以下命令行所示:

    cl /FeimportExample /std:c++latest /EHsc /nologo /W4 "%VCToolsInstallDir%\modules\std.ixx" importExample.cpp
    

    根据上一个命令行,编译器将生成名为 importExample.exe 的可执行文件。 运行它时,它会生成以下输出:

    Import the STL library for best performance
    555
    

使用 std.compat 导入标准库和全局 C 函数

C++标准库包括 ISO C 标准库。 该std.compat模块提供std模块的所有功能,例如std::vectorstd::coutstd::printfstd::scanf等等。 但它还提供这些函数的全局命名空间版本,例如::printf::scanf::fopen::size_t等等。

命名 std.compat 模块是一个兼容性层,用于轻松迁移引用全局命名空间中的 C 运行时函数的现有代码。 如果要避免向全局命名空间添加名称,请使用 import std;。 如果需要轻松迁移使用许多不限定的(全局命名空间)C 运行时函数的代码库,请使用 import std.compat;。 这提供了全局命名空间 C 运行时名称,因此无需使用 std:: 限定所有全局名称。 如果没有任何使用全局命名空间 C 运行时函数的现有代码,则无需使用 import std.compat;。 如果在代码中只调用了几个 C 运行时函数,则最好使用 import std; 并使用 std:: 来限定那些需要的少数全局命名空间 C 运行时名称。 例如,std::printf()。 如果在尝试编译代码时看到类似错误 error C3861: 'printf': identifier not found ,请考虑使用 import std.compat; 导入全局命名空间 C 运行时函数。

示例:如何生成和导入 std.compat

在使用 import std.compat; 之前,必须编译在源代码表单中找到的 std.compat.ixx模块接口文件。 Visual Studio 提供模块的源代码,以便可以使用与项目匹配的编译器设置编译模块。 这些步骤类似于生成 std 命名模块。 首先构建 std 命名的模块,因为 std.compat 依赖于它。

  1. 打开 VS 的本机工具命令提示符:在 Windows 开始 菜单中,键入 x86 本机 ,提示应显示在应用列表中。 确保提示为 Visual Studio 2022 版本 17.5 或更高版本。 如果使用错误版本的提示,则会收到编译器错误。

  2. 创建一个目录以尝试此示例,例如 %USERPROFILE%\source\repos\STLModules,并将其设为当前目录。 如果选择一个没有写入权限的目录,则会遇到错误。

  3. 使用以下命令编译stdstd.compat命名的模块:

    cl /std:c++latest /EHsc /nologo /W4 /c "%VCToolsInstallDir%\modules\std.ixx" "%VCToolsInstallDir%\modules\std.compat.ixx"
    

    您应该使用相同的编译器设置来编译 stdstd.compat,这些编译器设置也将用于导入它们的代码。 如果您有一个多项目解决方案,可以先编译一次,然后使用 /reference 编译器选项在所有项目中引用这些解决方案。

    如果收到错误,请确保使用正确的命令提示符版本。

    编译器输出前两个步骤中的四个文件:

    • std.ifc 是编译器查阅以处理 import std; 语句的已编译二进制命名模块接口。 编译器还会查阅 std.ifc 以处理 import std.compat;,因为 std.compat 是基于 std。 这是仅在编译时使用的工件。 它不会随应用程序一起交付。
    • std.obj 包含标准库的实现。
    • std.compat.ifc 是编译器查阅以处理 import std.compat; 语句的已编译二进制命名模块接口。 这是仅限编译时使用的工件。 它不会随应用程序一起交付。
    • std.compat.obj 包含实现。 但是,大部分实现由 std.obj 提供。 编译示例应用时,请 std.obj 添加到命令行,以静态方式将使用的功能从标准库链接到应用程序。

    可以使用以下开关控制对象文件名和命名模块接口文件名:

    • /Fo 设置对象文件的名称。 例如,/Fo:"somethingelse"。 默认情况下,编译器使用与要编译的模块源文件(.ixx)相同的对象文件的名称。 在本示例中,对象文件名是std.objstd.compat.obj默认的,因为我们正在编译模块文件和 std.ixxstd.compat.ixx
    • /ifcOutput 设置命名模块接口文件的名称(.ifc)。 例如,/ifcOutput "somethingelse.ifc"。 默认情况下,编译器将模块接口文件 (.ifc) 的名称与编译的模块源文件 (.ixx) 使用相同的名称。 在此示例中,默认生成的 ifc 文件是 std.ifcstd.compat.ifc,因为我们正在编译模块文件 std.ixxstd.compat.ixx
  4. 首先创建一个名为stdCompatExample.cpp的文件,并在其中填写以下内容以导入std.compat库:

    import std.compat;
    
    int main()
    {
        printf("Import std.compat to get global names like printf()\n");
    
        std::vector<int> v{5, 5, 5};
        for (const auto& e : v)
        {
            printf("%i", e);
        }
    }
    

    在前面的代码中, import std.compat; 替换 #include <cstdio>#include <vector>。 该语句 import std.compat; 使标准库和 C 运行时函数可用于一个语句。 导入此命名模块(包括 C++ 标准库和 C 运行时库的全局命名空间函数)比处理单个模块(例如 #include 模块 #include <vector>)要快。

  5. 使用以下命令编译示例:

    cl /std:c++latest /EHsc /nologo /W4 stdCompatExample.cpp
    link stdCompatExample.obj std.obj std.compat.obj
    

    我们不必在命令行上指定 std.compat.ifc ,因为编译器会自动查找与 .ifc 语句中的 import 模块名称匹配的文件。 当编译器遇到 import std.compat; 时,它会找到 std.compat.ifc,因为我们将其放在与源代码相同的目录中,这样就不需要在命令行上指定它了。 .ifc如果文件位于与源代码不同的目录中,或者具有其他名称,请使用/reference编译器开关来引用它。

    导入std.compat时,必须同时链接std.compatstd.obj,因为std.compat使用std.obj中的代码。

    如果要构建单个项目,可以通过将"%VCToolsInstallDir%\modules\std.ixx""%VCToolsInstallDir%\modules\std.compat.ixx"按顺序添加到命令行,来组合构建stdstd.compat标准库命名模块的步骤。 本教程演示了将标准库模块构建为单独的步骤,因为只需生成一次名为模块的标准库,然后就可以从项目或多个项目中引用它们。 但是,如果一次性生成它们很方便,请确保将它们放在使用它们的任何 .cpp 文件之前,并指定 /Fe 为生成 exe 命名,如以下示例所示:

    cl /c /FestdCompatExample /std:c++latest /EHsc /nologo /W4 "%VCToolsInstallDir%\modules\std.ixx" "%VCToolsInstallDir%\modules\std.compat.ixx" stdCompatExample.cpp
    link stdCompatExample.obj std.obj std.compat.obj
    

    在此示例中,编译源代码并将模块的实现链接到应用程序是单独的步骤。 他们不必非得这样。 你可以使用 cl /std:c++latest /EHsc /nologo /W4 stdCompatExample.cpp std.obj std.compat.obj 在一步中进行编译和链接。 但是,单独生成和链接可能很方便,因为在生成的链接步骤中,只需生成一次名为模块的标准库,然后就可以从项目或多个项目引用它们。

    上一个编译器命令生成一个名为stdCompatExample.exe的可执行文件。 运行它时,它会生成以下输出:

    Import std.compat to get global names like printf()
    555
    

标准库中命名模块的注意事项

命名模块的版本控制与标头的版本控制相同。 .ixx命名的模块文件与标头文件一起安装,例如:"%VCToolsInstallDir%\modules\std.ixx"文件,在撰写本文时使用的工具版本中,解析为C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.38.33130\modules\std.ixx。 挑选命名模块的版本, 方法和选择头文件版本相同——通过你引用它们的目录来实现。

不要将导入的头文件单元和命名模块混合使用。 例如,不要 import <vector>;import std; 在同一文件中。

不要混合和匹配导入C++标准库头文件和命名模块 stdstd.compat。 例如,不要 #include <vector>import std; 在同一文件中。 但是,可以在同一文件中包括 C 标头和导入命名模块。 例如,可以 import std;#include <math.h> 在同一文件中使用。 请勿包括C++标准库版本 <cmath>

不必担心重复导入模块。 也就是说,模块中不需要 #ifndef 样式标头防护。 编译器知道它是否已导入命名模块,并忽略重复尝试执行此作。

如果您需要使用 assert() 宏,请执行 #include <assert.h>

如果需要使用 errno 宏, #include <errno.h>。 由于命名模块不公开宏,因此例如在需要检查来自 <math.h> 的错误时,这是一个解决方法。

宏(例如 NANINFINITYINT_MIN)由 <limits.h> 定义,你可以包括这些宏。 但是,如果你import std;,可以使用std::numeric_limits<double>::quiet_NaN()std::numeric_limits<double>::infinity()而不是NANINFINITY,并使用std::numeric_limits<int>::min()而不是INT_MIN

概要

在本教程中,你已使用模块导入标准库。 接下来,了解如何在 命名模块教程中创建和导入自己的模块,C++。

另请参阅

比较标头单元、模块和预编译标头
C++中的模块概述
Visual Studio 中的C++模块教程
将项目移动到名为“Modules”的C++