透彻了解 inline的里里外外 绝不是那么简单

inline函数:比宏“安全”,不只是简单的替代,有参数的检验。但是却不需要一般函数调用的开销。另外,编译器的优化机制也会助你一臂之力,因为编译器的优化机制通常会用来浓缩那些“不含函数调用的”代码,因此,inline的函数也享受这份特权。

如此好用的东西,该尽量多用,为程序锦上添花,不然不然,简单的一个inline带给你的不只是效率,还有意想不到的烦恼。下面一一道来;


首先,目标码的大小肯定会增加,现在是将函数实现的本体直接嵌入到源代码该函数调用处。对于以下内存有限的机器,那么过多的inline就会导致磁盘的换页以及cache的低命中率,适得其反的效果。因此,如果inline的函数本体很小,则不会有这么些影响,但也不是告诉你只要函数本体小,就定义为inline。后面会讲到一个很重要的反例,构造函数。下面再详解。


其次,inline并不是命令,只是对编译器的一种建议,是否真的inling依旧取决于编译器

一般,inline函数可以通过直接在函数名前使用inline关键字即可;还有定义在类内部的函数也是inline的候选者。

例如:

class Person
{
public:    
    const int age(){return age;} //此函数即为inline函数
    /*....*/
private:
    int age;
};

最重要的是:

inline函数一般定义在头文件中,因为一般在编译的过程中就应该执行inlining过程。虽然有些环境不是,但是对于C++而言,inlining是编译期的行为。


很容易想到,inline对于虚函数则是不管用的了,因为虚函数只有在运行的时候才能决定具体是哪个函数。


再次:inline的函数有可能还是会有outlined版本,比如下面的代码;

inline void f() {/*...*/}
void (*pf)() = f; //pf 为指向函数f的函数指针

/*...*/
f(); // inlined
(*pf)(); //此调用可能不被inlined。



最后,终于要开始讲到 “构造函数以及析构函数绝对不是inline的绝佳候选人,即使函数体为空!!”


因为在构造函数以及析构函数期间,你可能不能预期“对象的构造以及析构期间都干了些什么!!!”

class Based
{
public:
    Based(){};
    
private:
    string Bs1, Bs2;
};

class Derived : public Based
{
public:
    Derived(){} //构造函数为空!!inline!!!
private:
    string Ds1, Ds2;
    
};


可以看到,Derived的构造函数为空,inline绝佳之选啊,于是inline了,可是,真的Derived的构造函数什么都不吗??显然不是!!!


在Derived构造函数调用期间,要做的首先的是Based构造函数的调用,以及各个成员变量(此处为string)的构造函数的调用,此处一共2次string构造函数调用,一次Based构造函数调用。更有甚者,如果Based的构造函数也是inline的,那么此处一共是4次string构造函数调用,一次Based构造函数调用。考虑到异常安全性的话,那么Derived的代码量,将不是肉眼看到的0行!!


至于析构函数,同样如此,逆序依次撤销。


因此。这里就有一个很重要的问题:什么时候将函数声明为inlined。

一般在调试时,一般不设置inline的,方便调试,因此此时你可以在你定义的函数本体设置断点,如果为inline的,将不可以,因为函数调用处的代码已经被完全替换掉了。


没有什么标准强制说什么函数可以inlined.建议将一些 经常调用的,并且函数体较小的设为inline。但是这绝不是命令,只是建议。


2014,5,21

updated:

关键词inline只是对编译器发出的请求,编译器是否处理此请求可能来源于函数本身的复杂度。“当编译器认为调用一个函数以及返回机制所带来的负荷高于其执行成本”时,编译器可能就会接受其为inline函数。

处理inline函数。分两步:

1 分析函数的定义,决定该函数的“本身固有的”inline性质。这其实就是编译器说了算。

如果该函数太复杂,而不能成为inline函数的话,那么编译器会将该函数转化为static函数。并产生该函数的定义。


2  如果该函数被接受为inline.那么编译器就会在  该函数被调用的每一点(称之为扩展点)上 ,带来 参数求值以及临时对象的管理。。。


值得注意的是,即使是在接受了该函数为inline之后,也就是在函数的扩展点上决定该函数是否是inline。这其实很费解,既然已经是inline了,那难道不是在每一处调用都扩展吗。其实不然,比如下面的例子:

//f函数为inline函数,

obj.f(lhs.f()+rhs.f()); 

那么在 《深入探索C++对象模型》中提到了这样的一句话,是说 inline函数如果只有一个表达式的话,那么其第二或者后继的调用就不会被扩展开。因此上述代码变为

obj.f = lhs.f + f_XXXX(&rhs); //其中的XXXX是C++编译器name mangling的结果。


在inline扩展期间,编译器会怎么做呢?

绝不是简简单单的把每个形式参数的值直接放到函数体中,一般来说,

对于会带来副作用的形式参数,会生成临时对象;如果实际参数是常量表达式,那么在替换之前会先完成其求值的操作。

比如下面的函数:

inline int minVal(int lhs, int rhs){return (lhs > rhs)?lhs:rhs;}

那么下面的3种调用方式,编译器采取的是不同的方式:

1  ret=minVal(10,5); -->编译器将之扩展为  ret=5;

2  ret=minVal(x,y); --> 扩展为 ret=(x>y)?x:y;

3  ret=minVal(x+y,z+y); -->扩展为 ret=(t1=x+y),(t2=z+y),(t1>t2)?t1:t2;


上述代码为了处理 形式参数的副作用,产生了临时对象,那么如果在inline函数定义内部本身就有局部变量的话,那么实际该inline函数会产生更多的临时对象。

同样的,inline函数的局部变量也是需要  name  mangling操作,因为必须保证inling函数的局部变量被放在函数调用的一个封闭的区段里面。


[updated] 2014.09.09

对于含有局部变量的inline函数,会怎么样呢?

inline int min(int i,int j)

{

  int minVal = i<j?i:j;

  return minVal;

}

这个局部变量会对inline函数的展开有什么影响呢?

例如下面的调用: 

{

  int local_var;

  int minVal;

...

  minVal = min(val1,val2);

}

会被展开成为:

{

  int local_var;

  int minVal;

  int _min_lv_minVal;

  minVal = (_min_lv_minVal, val1 < val2 ? val1:val2), _min_lv_minVal; //逗号表达式的值是最右边的表达式的值

}

inline函数的每一个局部变量都必须被放在函数调用的一个封闭区段中,拥有一个独一无二的名字,如果inline函数是以单一表达式扩展多次,那么每一次扩展都需要自己的一组局部变量。如果inline函数是以多个分离的式子扩展多次的话,那么只需要一组局部变量,可以重复使用。


inline函数的局部变量,加上有副作用的参数,可能会导致大量的临时对象。特别是它以单一表达式扩展多次的话(因为如果这样,局部变量是无法重用的)。


inline函数对于封装提供了必要的支持,可以有效存取封装于class中的non-public数据,并且是#define的有效替代品-特别是宏中的参数有副作用的时候,然而如果一个inline函数被调用多次的话,或产生大量的扩展码,增加程序的目标文件的大小。

值得一提的是,在继承层次较多的ctor中如果使用inline函数的话,将会导致扩展复杂度太高无法扩展开来。




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值