比较标头单元、模块和预编译标头

以前,可以使用 #include <vector> 这样的指令来包含标准库。 但是,包含头文件的成本很高,因为它们由包含它们的每个源文件进行重新处理。

引入了预编译标头 (PCH),通过翻译一次并重用结果来加速编译。 但预编译标头可能难以维护。

在 C++20 中,模块的引入是对头文件和预编译标头的重大改进。

标头单元是在 C++20 中引入的,用于暂时弥合头文件与模块之间的差距。 当你迁移代码以使用模块时,它们提供了模块的一些速度和可靠性优势。

然后,C++23 标准库引入了对将标准库导入为命名模块的支持。 这是使用标准库最快、最可靠的方式。

为了帮助你理清不同的选项,本文将传统的 #include 方法与预编译标头、标头单元和导入命名模块进行了比较。

下表按编译器处理速度和可靠性排列,#include 是最慢且最不可靠的,import 是最快且最可靠的。

方法 总结
#include 一个缺点是它们公开宏和内部实现。 内部实现通常公开为以下划线开头的函数和类型。 这是一种约定,表明某些内容是内部实现的一部分,不应使用。

头文件很脆弱,因为 #include 的顺序可能会修改行为或破坏代码,并且会受到宏定义的影响。

头文件编译速度缓慢。 特别是当多个文件包含同一个文件时,因为头文件会被多次重新处理。
预编译头 预编译标头 (PCH) 通过创建一组头文件的编译器内存快照来缩短编译时间。 这是对重复重新生成头文件的改进。

PCH 文件存在一些限制,导致它们难以维护。

PCH 文件的速度比 #include 快,但比 import 慢。
标头单元 这是 C++20 中的一项新功能,可用于将“行为良好”的头文件作为模块导入。

标头单元的速度比 #include 快,更易于维护,明显更小,并且也比预编译标头文件 (PCH) 更快。

标头单元是一个“中间”步骤,旨在帮助转换为命名模块,以防依赖头文件中定义的宏,因为命名模块不会公开宏。

标头单元比导入命名模块慢。

标头单元不受宏定义的影响,除非在生成标头单元时在命令行上指定它们,这使得它们比头文件更可靠。

标头单元会公开其中定义的宏和内部实现,就像头文件一样,而命名模块不会这样做。

作为文件大小的粗略近似值,250 MB 的 PCH 文件可由 80 MB 的标头单元文件表示。
模块 这是导入功能最快、最可靠的方式。

C++20 中引入了对导入模块的支持。 C++23 标准库引入了本主题中描述的两个命名模块。

导入 std 时,可获得标准名称,例如 std::vectorstd::cout,但没有扩展名,没有内部帮助程序(例如_Sort_unchecked),也没有宏。

导入顺序并不重要,因为没有宏或其他副作用。

作为文件大小的粗略近似值,250 MB 的 PCH 文件可由 80 MB 的标头单元文件表示,而这可由 25 MB 的模块表示。

命名模块速度更快,因为当命名模块编译为 .ifc 文件和 .obj 文件时,编译器会发成源代码的结构化表示形式,在导入模块时可以快速加载该表示形式。 编译器可以在发出 .ifc 文件之前执行一些工作(例如名称解析),因为命名模块与顺序和宏无关,因此在导入模块时不必完成这项工作。 相比之下,当通过 #include 使用头文件时,必须在每个翻译单元中一次又一次地预处理和编译其内容。

预编译标头是编译器内存快照,可以降低这些成本,但效果不如命名模块。

如果可以在应用中使用 C++20 功能和 C++23 标准库,请使用命名模块。

如果可以使用 C++20 功能但希望随着时间的推移过渡到模块,请在此期间使用标头单元。

如果无法使用 C++20 功能,请使用 #include 并考虑预编译标头。

另请参阅

预编译标头文件
C++ 中的模块概述
教程:使用模块导入 C++ 标准库
演练:导入 STL 库作为标头单位
演练:在 Visual C++ 项目中生成和导入标头单元