一. 内容
- inline 函数,多棒的点子!它们看起来像函数,动作像函数,比宏好用的多,见条款2,可以
调用它们又不需花费因调用函数带来的额外开销
。而且编译器最优化机制通常被设计来优化那些不含函数调用的代码,这意味编译器有能力对 inline 函数做语境相关的最优化
。 - 然后编写程序就像现实生活一样,没有白吃的午餐,inline函数也不例外。inline 函数背后的机制是:
将对此函数的每一个调用都用函数本体替换之
。这样无疑会增加产出码的大小
,在一台内存有限的机器上,过度热衷 inline 机制会造成程序体积太大,即使拥有虚内存,inline 造成的代码膨胀亦会导致额外的换页行为
,降低指令高速缓存装置的击中率,也就是cache miss
,以及伴随而来的效率损失。反之,如果函数本身足够小,其 inline 产出的代码比函数调用产出的代码要少,那么将函数声明为 inline 确实会提高运行效率。 - 记住,inline 只是对编译器的
一个申请,不是强制命令
。这项申请可以隐式提出,也可以明确提出。隐式方式是将函数定义在 class 声明式内
,这样的函数通常是成员函数,明确方式是在函数定义式前加上关键字 inline
。示例如下:
其中 SetNumber,GetNumber 函数为隐式 inline,Try 函数为显式 inline。class Inline { public: Inline(int mNumber): Number(mNumber) {} public: void SetNumber(int mNumber) { Number = mNumber; } int GetNumber() const { return Number; } private: int Number; }; inline void Try() { const Inline Inline(999); std::cout << Inline.GetNumber() << "\n"; }
inline 函数通常一定被置于头文件
。因为大多数构建环境在编译过程中进行 inlining,而为了将一个函数调用替换为被调用函数的本体,编译器必须知道那个函数长什么样子。只有少数某些构建环境可以在连接期完成,但毕竟是例外。inline 在大多数C++程序是编译期行为
。template 通常也被置于头文件内,因为它一旦被使用,编译器为了将它具现化,需要知道它长什么样子
。只有少数在连接期完成。template 在大多数C++程序也是编译期行为
。- 但 template 和 inline 是独立的,它们的
具现化彼此无关
,请根据自己的需要声明为 inline,避免 inline 不必要的成本。 inline 只是申请,不是强制要求
。对于那些过于复杂,如带有循环,递归的函数,编译器都会拒绝 inline,而对于就算最简单的 virtual 函数,编译器也拒绝 inline,这是很正常的,因为 virtual 需要等到运行期才能确定调用哪个函数,所以编译器无法在编译期获得 virtual 函数的本体用于替换调用行为,于是拒绝 inline。- 这些叙述综合起来的意思是:
一个表面上看似 inline 的函数 是否真的为 inline 取决于你的构建环境,取决于编译器
。幸运的是,编译器将会对于那些你要求 inline 却 无法 inline 的函数提供警告信息,用于诊断。 - 但是有时候编译器虽然有意愿 inline 某个函数,但还是可能为该函数生成一个函数本体。举个例子,
如果程序要取某个 inline 函数的地址,编译器通常必须为此函数生成一个 outlined 函数本体
。毕竟编译器没有能力提出一个指针指向一个并不存在的函数。与此并提的是:编译器通常不对通过函数指针而进行的调用实施 inline。这意味着对 inline 函数的不同调用方式也决定了实际是否 inline。 - 但是即使你从未使用函数指针,编译器仍然可能不会 inline 一些函数,因为程序员不是唯一要求函数指针的人,有时候编译器会生成构造函数和析构函数的 outlined 副本,用于获得它们的指针去在数组内部的构造和析构过程中调用。实际上
构造函数和析构函数往往是 inline 的糟糕人选
,因为即使一个表面上看似空的构造函数,实际上编译器为此可能做了很多默认的工作。C++对于对象被构造很销毁时发生什么事做了各式各样的保证。当你创建一个对象,其每一个base class及成员变量会自动构造,当你删除一个对象,反向程序的析构行为亦会自动发生。在这些情况下,C++描述了什么一定会发生,但没有说如何发生。事情如何发生是由编译器的职责,不过有一点很情况,它们不会凭空发生,一定是有某些代码让它们发生,而那些代码:是由编译器在编译期代为产生安插到你的程序中去的,肯定存在于某些地方,有些时候就存在于构造函数和析构函数之中。所以我们可以想象,之前那个看似为空的构造函数实际上产生了大量的代码。 - 程序设计者必须评估将函数申明为 inline 的冲击:
inline 函数无法随着程序库的升级而升级
。换句话说,如果 fuc 是程序库中的一个 inline 函数,客户将其函数本体编进程序中,一旦程序设计者决定改变 fuc,所有用到 fuc 的客户代码都必须重新编译。这往往是大家不愿意看到的。然而如果 fuc 是 non-inline 函数,一旦有某些修改,客户只需要重新连接即可,远比重新编译的负担少得多。事实上,如果程序库采取动态连接,升级版函数甚至可以不知不觉的被应用程序所接纳。 - 对于程序开发来说,
大部分调试器都无法对 inline 函数进行调试
,毕竟如何在一个并不存在的函数内设立断点呢?虽然某些构建环境勉强支持 inline 函数的调试,大多数构建环境仅仅只能在调试期禁止 inlining 行为。
二. 总结
- 将大多数 inlining 限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级(binary upgradability)更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
- 不要只因为 function template 出现在头文件,就将它们声明为 inline。