什么时候只有标题库可以接受?

时间:2010-02-01 04:29:43

标签: c++ header-files

就个人而言,我非常喜欢仅限标题的库,但有些声称它们会因为过度内联而导致代码膨胀(以及编译时间较长的另一个明显问题)。

我在想,这些说法有多少真相(关于臃肿的说法)?

此外,成本是否合理? (显然有一些不可避免的情况,例如它是纯粹或大部分使用模板实现的库,但是我对实际可用选项的情况更感兴趣。)

我知道就这样的事情而言,没有严格的规则,准则等,但我只是想了解别人对这个问题的看法。

P.S。是的,这是一个非常模糊和主观的问题,我知道,因此我已将其标记为。

4 个答案:

答案 0 :(得分:7)

我为一家拥有“中间件”部门的公司工作,以维护数百个众多团队常用的图书馆。

尽管我们在同一家公司工作,但由于易于维护,我们不喜欢仅限标题的方法而更倾向于支持二进制兼容性而不是性能。

普遍的共识是,性能提升(如果有的话)不值得麻烦。

此外,所谓的“代码膨胀”可能会对性能产生负面影响,因为要在缓存中加载更多代码意味着更多缓存未命中,这些都是性能杀手。

在一个理想的世界中,我认为编译器和链接器可能足够智能而不能生成那些“多个定义”规则,但只要不是这样,我会(个人)赞成:

  • 二进制兼容性
  • 非内联(对于超过几行的方法)

你为什么不测试?准备两个库(只有一个标题,另一个没有在几行内联方法),并检查它们各自的性能。

修改

'jalf'(谢谢)已经指出我应该通过二进制兼容性来确切地说明我的意思。

如果您可以(通常)链接到一个或另一个而不更改您自己的库,则给定库的2个版本称为二进制兼容。

因为您只能链接到给定库Target的一个版本,所以使用Target加载的所有库将有效地使用相同的版本......这就是导致传递性的原因这个属性。

MyLib --> Lib1 (v1), Lib2 (v1)
Lib1 (v1) --> Target (v1)
Lib2 (v1) --> Target (v1)

现在,我们需要Target针对Lib2仅使用的功能进行修复,我们会提供新版本(v2)。如果(v2)(v1)二进制兼容,那么我们就可以:

Lib1 (v1) --> Target (v2)
Lib2 (v1) --> Target (v2)

但如果情况并非如此,那么我们将:

Lib1 (v2) --> Target (v2)
Lib2 (v2) --> Target (v2)

是的,你读得对,即使Lib1不需要修复,你仍然要针对新版本的Target重建它,因为这个版本对于更新的{{1}是强制性的}和Lib2只能链接到Executable的一个版本。

使用仅限标头的库,由于您没有库,因此实际上不是二进制兼容的。因此,每次进行一些修复(安全性,严重错误等等)时,您需要提供一个新版本,并且所有依赖于您的库(甚至是间接的)都必须针对这个新版本重建!

答案 1 :(得分:6)

根据我的经验,膨胀不是问题:

  • 仅头文件库使编译器具有更强的内联能力,但它们不会强制编译器内联 - 许多编译器将内联关键字视为忽略多个相同定义的命令。

  • 编译器通常有优化选项来控制内联量;微软编译器上的/ Os。

  • 通常情况下,允许编译器管理速度与大小问题通常会更好。您只能看到实际内联的调用中的膨胀,并且编译器只会在它的启发式指示其内联将提高性能时内联它们。

我不会认为代码膨胀是远离标题库的理由 - 但我建议你考虑一下只有标题的方法会增加编译时间。

答案 2 :(得分:3)

我同意,内联库更容易使用。

内联膨胀主要取决于您正在使用的开发平台 - 特别是编译器/链接器功能。我不认为它会成为VC9的主要问题,除非在一些极端情况下。

我在一个大型VC6项目的某些地方看到了最终大小的一些显着变化,但很难给出具体的“可接受的,如果......”。你需要在你的devenv中尝试使用你的代码。

第二个问题可能是编译时间,即使使用预编译的头文件(也存在权衡)。

第三,一些结构是有问题的 - 例如静态数据成员在翻译单元之间共享 - 或避免在每个翻译单元中使用单独的实例。


我已经看到以下机制给用户一个选择:

// foo.h
#ifdef MYLIB_USE_INLINE_HEADER
#define MYLIB_INLINE inline
#else 
#define MYLIB_INLINE 
#endif

void Foo();  // a gazillion of declarations

#ifdef MYLIB_USE_INLINE_HEADER
#include "foo.cpp"
#endif

// foo.cpp
#include "foo.h"
MYLIB_INLINE void Foo() { ... }

答案 3 :(得分:1)

过度内联可能是调用者应该解决的问题,调整他们的编译器选项,而不是被调用者试图通过inline关键字和标题中的定义的非常钝的工具来控制它。例如,GCC有-finline-limit和朋友,因此您可以为不同的翻译单元使用不同的内联规则。根据架构,指令缓存大小和速度,函数的使用方式等因素,对你来说过度内联可能不会过度内联。不是我曾经需要做这个调整:在实践中它有它值得担心的是,它值得重写,但这可能是巧合。无论哪种方式,如果我是一个库的用户,那么在其他条件相同的情况下,我宁愿选择内联(受我的编译器限制,我可能不会占用)而不能内联。

我认为来自仅限标头库的代码膨胀的恐惧更多地来自担心链接器将无法删除冗余的代码副本。因此,无论函数是否实际在调用站点内联,关注的是您最终会使用每个使用它的目标文件的函数(或类)的可调用副本。我不记得C ++中不同翻译单元中的内联函数的地址是否必须比较相等,但即使假设它们也是如此,因此在链接代码中有一个“规范”的函数副本,它不一定意味着链接器实际上将删除死的重复函数。如果仅在一个翻译单元中定义该功能,则可以合理地确信每个静态库或使用它的可执行文件只有一个独立副本。

老实说,我不知道这种恐惧是多么有根据。我所做的每件事都要么受到严格的内存限制,我们只使用inline作为static inline函数这么小,以至于我们不希望内联版本明显比代码要大打电话,不介意重复,或者如此松散地限制我们不关心任何地方的重复。我从来没有在各种不同的编译器上寻找并计算重复数据的中间地带。我偶尔会从其他人那里听说模板代码存在问题,所以我相信声明中存在真相。

我现在继续这样做,我认为如果你发送一个只有标题的库,如果用户不喜欢它,用户总是可以使用它。编写一个声明所有函数的新标题和一个包含定义的新翻译单元。类中定义的函数必须移动到外部定义,因此如果要支持此用法而不要求用户分叉代码,则可以避免这样做并提供两个头:

// declare.h
inline int myfunc(int);

class myclass {
    inline int mymemberfunc(int);
};

// define.h
#include "declare.h"
int myfunc(int a) { return a; }

int myclass::mymemberfunc(int a) { return myfunc(a); }

担心代码膨胀的调用者可能会在其所有文件中包含declare.h,然后编写:

// define.cpp
#include "define.h"

他们可能还需要避免整个程序优化以确保代码不会被内联,但是你不能确定即使是非内联函数也不会被整个程序优化内联。

不担心代码膨胀的调用者可以在其所有文件中使用define.h。