【问题标题】:Benefits of header-only libraries仅头文件库的好处
【发布时间】:2012-09-22 04:34:21
【问题描述】:

只有头文件的库有什么好处?为什么你会以这种方式编写它而不是将实现放入单独的文件中?

【问题讨论】:

标签: c++ header-only


【解决方案1】:

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

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

您也可以分发实现文件,但这意味着用户需要额外的步骤 - 在使用之前编译您的库。

当然,这适用于逐案。例如,仅头文件库有时会增加 代码大小和 编译时间。

【讨论】:

  • “拥有一个仅包含头文件的库也意味着您不必担心可能使用该库的不同平台”:前提是您不必维护该库。否则,这是一场噩梦,您无法在现有材料上重现或测试错误报告。
  • 我刚刚问了一个类似的问题,关于仅标头的性能优势。如您所见,代码大小没有区别。但是,示例仅标头实现的运行速度慢了 7%。 stackoverflow.com/questions/12290639/…
  • @Homer6 感谢您联系我。我从来没有真正测量过这个。
  • @LuchianGrigore 我也找不到其他人。这就是为什么花了一段时间才回答的原因。有很多推测性的“增加代码大小”和“内存消耗”的 cmets。我终于有了差异的快照,即使这只是一个例子。
  • @Homer6。为什么它不会增加代码大小?假设您创建了多个仅使用标头库的库,然后您的应用程序使用了所有这些库,那么您将必须拥有多个副本,而不是链接到单个共享库。
【解决方案2】:

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

【讨论】:

    【解决方案3】:

    仅标头库的好处:

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

    仅头文件库的缺点:

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

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

    • 比较纠结的编译。由于仅标头库需要额外的#includes,因此您会在仅标头库中获得更多依赖项。更改库中某些关键功能的实现,您可能需要重新编译整个项目。对已编译库的源文件进行更改,您所要做的就是重新编译该库源文件,使用新的 .o 文件更新已编译库,然后重新链接应用程序。

    • 人类更难阅读。即使有最好的文档,图书馆的用户也经常不得不求助于阅读图书馆的标题。仅标头库中的标头充满了妨碍理解接口的实现细节。使用已编译的库,您所看到的只是接口和对实现功能的简短评论,而这通常就是您想要的。这就是你应该想要的。您无需了解实现细节即可知道如何使用该库。

    【讨论】:

    • 最后一点没有什么意义。任何合理的文档都将包括函数声明、参数、返回值等以及所有相关的 cmets。如果非要参考头文件,说明文档失败。
    • @Thomas - 即使拥有最好的专业图书馆,我也经常发现自己不得不求助于阅读“精美”的标题。事实上,如果从代码加上注释中提取出所谓的“精细”文档,我通常喜欢阅读标题。代码加上 cmets 比自动生成的文档更能告诉我。
    • 最后一点无效。私有成员中的头已经填充了实现细节,所以它不像 cpp 文件隐藏了所有的实现细节。此外,像 C# 这样的语言在设计上本质上是“仅标题”,IDE 负责隐藏细节(​​“折叠”它们)
    • @Tomas:同意,最后一点完全是假的。您可以轻松地将接口和实现与仅标头库分开;您只需拥有接口标头#include 实现细节。这就是为什么 Boost 库通常包含一个名为 detail 的子目录(和命名空间)。
    • @Thomas:我不同意。头文件通常是我查找文档的第一个地方。如果标题写得好,通常不需要外部文档。
    【解决方案4】:

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

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

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

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

    在谈到 CRT 时,您的库和用户的源代码通常需要链接到同一个 CRT。使用 Visual Studio,如果您使用多线程 CRT 构建库,但用户链接到多线程调试 CRT,那么您将遇到链接问题,因为您的库可能找不到它需要的符号。我不记得它是哪个函数,但是对于 Visual Studio 2015,Microsoft 内联了一个 CRT 函数。突然,它出现在标头而不是 CRT 库中,因此希望在链接时找到它的库不再能够做到这一点,这会产生链接错误。结果是这些库需要使用 Visual Studio 2015 重新编译。

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

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

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

    如果您提供仅标头库,那么所有代码​​都会使用相同的编译器设置和相同的标头进行编译,因此很多这些问题都会消失(提供您和您的用户使用的第三方库的版本与 API 兼容)。

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

    【讨论】:

      【解决方案5】:

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

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

      一个最小的具体例子显示在:Link-time optimization and inline

      所以你只需传递一个标志,就可以跨目标文件进行内联,无需任何重构工作,不再需要在标题中保留定义。

      LTO 也可能有其自身的缺点:Is there a reason why not to use link-time optimization (LTO)?

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2023-03-03
        • 1970-01-01
        • 2017-02-14
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-12-17
        • 1970-01-01
        相关资源
        最近更新 更多