个人而言,我相当喜欢只有头文件的库,但有人声称它们会导致代码膨胀,原因是过度内联(以及更长的编译时间等其他明显问题)。
我想知道这些说法有多少真实性(关于膨胀的说法)。
此外,这些成本是否“合理”?(显然有无法避免的情况,比如它是仅使用模板实现的库,但我更感兴趣的是有选择余地的情况。)
我知道在这方面没有硬性规定、指南等,但我只是想了解别人对这个问题的看法。
附言:是的,这是一个非常模糊和主观的问题,我知道,并且已经标记为这样的问题。
个人而言,我相当喜欢只有头文件的库,但有人声称它们会导致代码膨胀,原因是过度内联(以及更长的编译时间等其他明显问题)。
我想知道这些说法有多少真实性(关于膨胀的说法)。
此外,这些成本是否“合理”?(显然有无法避免的情况,比如它是仅使用模板实现的库,但我更感兴趣的是有选择余地的情况。)
我知道在这方面没有硬性规定、指南等,但我只是想了解别人对这个问题的看法。
附言:是的,这是一个非常模糊和主观的问题,我知道,并且已经标记为这样的问题。
根据我的经验,代码膨胀并不是一个问题:
仅有头文件库给编译器提供了更大的能力来进行内联,但它们并不会强制编译器进行内联操作——很多编译器将`inline`关键字视为忽略多个相同定义的命令。
编译器通常有优化选项来控制内联的数量。例如,在微软的编译器上可使用`/Os`。
通常最好允许编译器管理速度和大小之间的问题。只有当被调用的函数实际上已经被内联后,你才会看到代码膨胀,而编译器只会在其启发式算法表明内联可以提高性能时才进行内联操作。
我不认为代码膨胀是避免使用仅有头文件库的原因,但我会建议你考虑一下头文件库会增加编译时间的程度。
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
重新构建它,因为这个版本是更新的Lib2
所必需的,而Executable
只能链接一个版本的Target
。
对于头文件库来说,由于您没有库,因此实际上不具有二进制兼容性。因此,每次进行一些修复(安全性、关键错误等),您都需要提供一个新版本,并且所有依赖于您的库(甚至是间接的)都必须针对这个新版本进行重建!
inline
,则无法依赖链接器将它们合并在一起。你是正确的,如果允许代码的不同部分看到函数的不同版本,那么这就构成了明显的 ODR 违规,你将需要花费很多时间进行调试。但是这种情况可能会发生,无论是否使用头文件库,并且假设在更改其依赖的头文件时不重新编译代码。我理解你想说什么,但我不同意这是头文件库固有的问题。 - jalf我认为,内联库更易于使用。
内联库的膨胀程度主要取决于您正在使用的开发平台,具体而言,取决于编译器/链接器的能力。除非出现少数特殊情况,否则我不认为在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() { ... }
我认为来自仅包含头文件的库的代码膨胀的恐惧更多地来自于担心链接器无法删除冗余的代码副本。因此,无论函数是否在调用站点内联,关注点都是您最终会得到一个可调用的函数(或类)副本,每个使用它的对象文件都有一个。我不记得在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); }
// define.cpp
#include "define.h"
他们可能还需要避免整个程序优化,以确保代码不会被内联,但是这样一来,即使非内联函数也无法保证不会被整个程序优化内联。
不担心代码膨胀的调用者可以在所有文件中使用define.h。