仅标头库的优点

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

60633 次浏览

在某些情况下,只使用标题库是唯一的选择,例如在处理模板时。

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

您还可以分发实现文件,但是这意味着用户在使用库之前需要额外的一个步骤。

当然,这适用于 个别情况。例如,只有标头的库有时会增加 代码大小 & 编译的时间。

主要的“好处”是它需要您交付源代码,因此 你最终会得到机器和编译器的错误报告 当库完全是模板的时候,你没有 很多选择,但是当你有选择的时候,标题通常只是一个穷人 工程选择。(另一方面,当然,标题只意味着 您不必记录任何集成过程。)

头文件库的好处:

  • 简化了构建过程。您不需要构建库,也不需要在构建的链接步骤中指定已编译的库。如果您确实有一个已编译的库,那么您可能希望构建它的多个版本: 一个已编译并启用了调试,另一个已启用了优化,可能还有另一个删除了符号。对于一个多平台系统来说甚至可能更多。

仅标头库的缺点:

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

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

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

  • 人类很难读懂。即使使用最好的文档,图书馆的用户通常也不得不阅读图书馆的标题。只有标头的库中的标头充满了阻碍理解接口的实现细节。对于编译后的库,您所看到的只是接口和关于实现功能的简短说明,这通常就是您所需要的全部内容。这才是你想要的。您不必了解实现细节就可以知道如何使用该库。

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

这基本上是基于这样一个概念: 您要么编写一个带有标题的库以分发给用户,要么重用自己,而不是将所有内容都放在标题中。如果您正在考虑重用头文件和源文件,并在每个项目中重新编译这些文件,那么这并不适用。

基本上,如果你编译你的 C + + 代码并且用一个编译器构建一个库,那么用户就会尝试用不同的编译器或者同一个编译器的不同版本来使用这个库,然后你可能会得到链接器错误或者由于二进制不兼容而导致的奇怪的运行时行为。

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

在讨论 CRT 时,您的库和用户的源代码通常需要链接到相同的 CRT。在 Visual Studio 中,如果您使用多线程 CRT 构建库,但是用户链接到多线程调试 CRT,那么您将遇到链接问题,因为您的库可能找不到它需要的符号。我不记得是哪个函数了,但是微软为 VisualStudio2015做了一个内联 CRT 函数。突然之间,它出现在头部而不是 CRT 库中,所以那些希望在链接时找到它的库不再能够做到这一点,这就产生了链接错误。结果是这些库需要使用 VisualStudio2015进行重新编译。

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

对于从其他第三方库(例如 Eigen 向量或 GSL 矩阵)跨库边界传递对象/类型也是如此。如果第三方库在您编译库和您的用户编译他们的代码之间更改了标题,那么事情就会中断。

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

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

但是,上面提到过一些负面因素,例如增加了编译时间。此外,您可能正在经营一家企业,因此您可能不希望将所有源代码实现细节交给所有用户,以防其中一个用户窃取它。

内联可以通过链路时间优化(LTO)完成

我想强调这一点,因为它降低了仅头库的两个主要优点之一的价值: “您需要在头上定义内联”。

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

所以你只需要传递一个标志,内联就可以在不需要任何重构工作的情况下跨对象文件进行,不需要再在头中保留定义,这样就可以减慢自动重新构建包含器的构建系统中的编译时间。

然而,LTO 也可能有它自己的缺点: 为什么不使用链路时间优化(LTO) ?