前言
inline函数无论是看起来还是用起来都比宏更像函数而且更好用,调用它们又无需承受函数调用的额外开销。另外,编译器会对不含函数调用的程序执行语境相关最优化。然而,inline函数并不是完美无缺的。
inline的成本
inline的本质是“对函数的每一个调用”都以函数本体替换,这无疑增加了目标码(object code)大小,过度热衷inline可能会导致程序体积太大,随之导致额外的换页行为(page过多)。但是,如果inline函数很小,可能“函数本体”比“函数调用”所产出的代码更小,那当然是再好不过了。
inline申请
inline只是对编译器提出的申请。这个申请可以显式提出也可以隐式提出。
隐式inline
隐式就是把函数定义在class内部。一般而言这种隐式申请都是成员函数,不过friend函数也能被定义在class内(Effective C++ 47),如果真是那样,那它们也被隐式声明为inline.
显式inline
显式声明方法是在函数定义式前加上inline。以<algorithm>中的max template
为例:1
2
3
4template<typename T>
inline const T& max(const T& a,const T& b){
return a<b?b:a;
}
inline与template
inline函数和template函数通常都会被定义在头文件中,所以有人会误以为function template一定是inline.这一点是错误的。
inline放在头文件的原因很简单,编译器需要在编译期将某个“函数调用”替换为“被调用函数的本体”。
template通常也放在头文件里,因为它一旦被使用,编译器为了具现化必须知道它的样子。
总之,template的具现化与inlining无关,如果你觉得所有据此template产生的function应该inline,那就声明它为inline,否则不必如此。
编译器决定是否inline
编译器拒绝将过于复杂的函数inline(带有循环或者递归),并且任何virtual函数的调用也会使inline落空。这是十分显然的,因为virtual意味着运行时才知道调用哪个函数,inline却是在编译期就必须确定。不过幸运的是,如果编译器拒绝了你的inline请求,会反馈一个警告信息。
有时候虽然编译器愿意inlining某个函数,但还是可能为该函数生成一个函数本体。
举例而言,如果某个函数需要取某一个inline函数的地址,那编译器会生成一个outlined函数本体,毕竟编译器无法去提出一个指针指向一个不存在的函数。1
2
3
4
5inline void f() {..};
void (*pf)() = f;//pf指向f
...
f();//确实inline
pf();//生成了函数实体并产生了调用
inline与构造、析构函数
假设有base class与derived class如下:1
2
3
4
5
6
7
8
9
10
11class Base{
public:
...
private:
string str;
};
class Derived::public Base{
public:
Derived() {}//空构造函数
...
};Derived::Derived()
看起来似乎很适合被inline,因为它根本不含任何代码,但实际并非如此。
构造或者析构函数内部存在着大量处理异常的程序,并且肯定存在着Base类的构造函数或者Derived析构函数。对这些函数的调用影响了编译器是否会对此空白函数inlining.
此外,这些代码如果被inline,那他们必然化身为object code充斥着各类Derived class.
总而言之,构造函数和析构函数不适合被声明为inline.
inline与程序库
程序库设计者使用inline之前必须考虑其影响性:inline无法随着程序库升级而升级。也就是说如果一旦某一个inline函数被改写,所有用到它的客户端程序都要重新编译。如果是non-inline,那用户只需要重新连接就好了。
inline与debug
我们无法在inline函数中设置断点,因为实际上并不存在这个函数。
总结
- 将inline函数限制在小型、频繁调用的函数身上,这会使潜在的代码膨胀问题最小化,程序速度提升机会最大化。
- 不要因为function template出现在头文件就把它声明为inline,这二者并不相关。