教程:使用命令行中的模块导入 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() 函数的内容。 不会导出全局命名空间(如 ::printf())中定义的 C 函数。 这可改善包含 <cstdio> 这样的 C 包装器标头的同时也会包含像 stdio.h 这样的 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 时,诸如 asserterrnooffsetofva_arg 等宏不可用。 有关解决方法,请参阅标准库命名模块注意事项

关于 C++ 模块

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

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

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

有关模块的详细信息,请参阅 C++ 中的模块概述。本文还讨论了将 C++ 标准库用作模块,但使用较旧的和实验性的方式来执行此操作。

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

使用 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 命名模块。 如果你有多项目解决方案,则可以编译名为模块的标准库一次,然后使用 /reference 编译器选项从所有项目引用它。

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

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

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

    开关 含义
    /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"。 默认情况下,编译器使用与要编译的模块源文件 (.ixx) 相同的模块接口文件 (.ifc) 名称。 在此示例中,生成的 ifc 文件默认为 std.ifc,因为我们正在编译模块文件 std.ixx
  4. 导入你生成的 std 库,方法是先创建包含以下内容的名为 importExample.cpp 的文件:

    // 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",因为编译器会自动查找与 import 语句指定的模块名称匹配的 .ifc 文件。 当编译器遇到 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 标准库命名模块的步骤和生成应用程序的步骤。 将其放在使用 std 模块的任何 .cpp 文件之前。

    默认情况下,输出可执行文件的名称取自第一个输入文件。 使用 /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.compatstd 上生成。 这是仅编译时的项目。 它不会随应用程序一起交付。
    • 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.obj
    • /ifcOutput 设置命名模块接口文件 (.ifc) 的名称。 例如 /ifcOutput "somethingelse.ifc"。 默认情况下,编译器使用与要编译的模块源文件 (.ixx) 相同的模块接口文件 (.ifc) 名称。 在此示例中,生成的 ifc 文件默认为 std.ifcstd.compat.ifc,因为我们正在编译模块文件 std.ixxstd.compat.ixx
  4. 导入 std.compat 库,方法是先创建包含以下内容的名为 stdCompatExample.cpp 的文件:

    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,因为编译器会自动查找与 import 语句中的模块名称匹配的 .ifc 文件。 当编译器遇到 import std.compat; 时,它会发现 std.compat.ifc,因为我们将其放在与源代码相同的目录中,从而减轻了我们在命令行上指定它的需求。 如果 .ifc 文件位于与源代码不同的目录中,或使用不同的名称,请使用 /reference 编译器开关来引用它。

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

    如果要生成单个项目,可以将生成 stdstd.compat 标准库命名模块的步骤相结合,方法是将 "%VCToolsInstallDir%\modules\std.ixx""%VCToolsInstallDir%\modules\std.compat.ixx"(按该顺序)添加到命令行。 本教程演示了作为单独步骤生成标准库模块,因为只需生成一次名为模块的标准库,然后就可以从项目或多个项目中引用它。 但是,如果一次生成它们很方便,请确保将其放在使用它们的任何 .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;,则可以使用 numeric_limits<double>::quiet_NaN()numeric_limits<double>::infinity(),而不是 NANINFINITYstd::numeric_limits<int>::min(),而不是 INT_MIN

总结

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

另请参阅

比较标头单元、模块和预编译标头
C++ 中的模块概述
Visual Studio 中的 C++ 模块简介
将项目移动到 C++ 命名模块