仅表头库的优点

Benefits of header-only libraries

本文关键字:表头      更新时间:2023-10-16

只有头的库有什么好处?为什么要用这种方式编写它,而不是将实现放在单独的文件中?

仅头库的优点:

  • 简化了构建过程。您不需要构建库,也不需要在构建的链接步骤中指定已编译的库。如果你有一个编译过的库,你可能会想要构建它的多个版本:一个是在启用调试的情况下编译的,另一个是启用优化的,还有一个可能是去掉符号的。对于一个多平台系统来说,可能会更多

仅头库的缺点:

  • 更大的对象文件。在某个源文件中使用的库中的每个内联方法也会在该源文件的编译对象文件中得到一个弱符号、越界定义。这降低了编译器的速度,也降低了链接器的速度。编译器必须生成所有膨胀,然后链接器必须将其过滤掉。

  • 更长的汇编。除了上面提到的膨胀问题外,编译还需要更长的时间,因为纯头库的头本质上比编译库的头更大。对于使用该库的每个源文件,都需要解析这些大标头。另一个因素是,只有头的库中的那些头文件必须是内联定义所需的#include头,以及将库构建为编译库所需的头。

  • 更复杂的汇编。由于只有头的库需要额外的#include,所以只有头的库会获得更多的依赖关系。更改库中某些关键函数的实现,您可能需要重新编译整个项目。对已编译库的源文件进行更改,所要做的就是重新编译该库源文件,用新的.o文件更新已编译库,并重新链接应用程序。

  • 人类更难阅读。即使有最好的文档,库的用户也经常不得不阅读库的标题。只有头的库中的头填充了妨碍理解接口的实现细节。对于编译后的库,您所看到的只是接口和对实现功能的简短评论,而这通常就是您想要的。这才是你真正想要的。您不需要知道实现的详细信息就可以知道如何使用库。

在某些情况下,只有头库是唯一的选项,例如在处理模板时。

拥有一个只有头的库也意味着你不必担心可能使用该库的不同平台。分离实现时,通常会隐藏实现详细信息,并将库作为标头和库(libdll.so文件)的组合进行分发。当然,这些必须针对您提供支持的所有不同操作系统/版本进行编译。

您也可以分发实现文件,但这对用户来说意味着一个额外的步骤——在使用库之前编译库

当然,这适用于的具体情况。例如,只有头的库有时会增加

的代码大小&编译时间。

我知道这是一个旧线程,但没有人提到ABI接口或特定的编译器问题。所以我想我会的。

这基本上是基于这样一个概念,即你要么编写一个带有头的库来分发给人们,要么重用自己,而不是把所有东西都放在头里。如果您正在考虑重用头文件和源文件,并在每个项目中重新编译它们,那么这并不适用。

基本上,如果你编译C++代码并用一个编译器构建一个库,然后用户试图用不同的编译器或同一编译器的不同版本使用该库,那么由于二进制不兼容,你可能会出现链接器错误或奇怪的运行时行为。

例如,编译器供应商经常在不同版本之间更改STL的实现。如果库中有一个函数接受std::vector,那么它希望该类中的字节按照编译库时的排列方式排列。如果在新的编译器版本中,供应商对std::vector进行了效率改进,那么用户的代码就会看到可能具有不同结构的新类,并将该新结构传递到库中。从那以后一切都在走下坡路。。。这就是为什么建议不要跨越库边界传递STL对象。这同样适用于C运行时(CRT)类型。

当谈到CRT时,您的库和用户的源代码通常需要链接到同一CRT。使用Visual Studio,如果使用多线程CRT构建库,但用户链接多线程调试CRT,则会出现链接问题,因为库可能找不到所需的符号。我记不清是哪个函数了,但对于Visual Studio 2015,微软制作了一个内联CRT函数。突然间,它出现在标题中,而不是CRT库中,所以希望在链接时找到它的库再也做不到了,这就产生了链接错误。结果是,这些库需要使用Visual Studio 2015重新编译。

如果您使用Windows API,但使用与库用户不同的Unicode设置进行构建,则也可能会出现链接错误或奇怪行为。这是因为Windows API具有使用Unicode或ASCII字符串的函数,以及根据项目的Unicode设置自动使用正确类型的宏/定义。如果您在库边界上传递了一个错误类型的字符串,那么在运行时就会中断。或者你可能会发现程序一开始就没有链接。

对于从其他第三方库(例如特征向量或GSL矩阵)跨越库边界传递对象/类型,这些情况也是正确的。如果第三方库在您编译库和用户编译代码之间更改了它们的头,那么事情就会崩溃。

基本上为了安全起见,您可以跨越库边界的唯一东西是内置类型和纯旧数据(POD)。理想情况下,任何POD都应该在您自己的标头中定义的结构中,并且不依赖于任何第三方标头。

如果你提供了一个只有头的库,那么所有的代码都会使用相同的编译器设置并针对相同的头进行编译,所以很多问题都会消失(提供你和你的用户使用的第三部分库的版本是API兼容的)。

然而,上面已经提到了一些负面因素,比如编译时间的增加。此外,您可能正在运营一家企业,因此您可能不想将所有源代码实现细节交给所有用户,以防其中一人窃取。

主要的"好处"是它需要交付源代码,因此你最终会得到机器上的错误报告和编译器从未听说过。当库完全是模板时有很多选择,但当你有选择的时候,只有标题通常是很糟糕的工程选择。(当然,另一方面,页眉仅表示您不必记录任何集成过程。)

插入可以通过链路时间优化(LTO)来完成

我想强调这一点,因为它降低了纯头库的两个主要优点之一的值:;您需要在页眉上定义以内联";。

这方面的一个最简单的具体例子显示在:链接时间优化和内联

因此,您只需传递一个标志,就可以在对象文件之间进行内联,而无需进行任何重构工作,也无需再将定义保留在标头中,这可能会降低自动重建includer的构建系统的编译时间。

LTO可能也有自己的缺点:有没有理由不使用链路时间优化(LTO)?