4.5 Inline Functions
以下是Point class 的一个加法运算符的可能实现内容:class Point {
friend Point operator+(const Point&, const Point&);
};
Point operator+(const Point &lhs, const Point &rhs) {
Point new_pt;
new_pt._x = lhs._x + rhs._x;
new_pt._y = lhs._y + rhs._y;
return new_pt;
}
理论上,一个比較"干净"的做法是使用 inline 函数set和get函数来完毕.
// void Point::x(float new_x) { _x = new_x; }
// float Point::x() { return _x; }
new_pt.x(lhs.x() + rhs.x());
因为受限仅仅能在上述两个函数中对_x直接存取,因此也就将稍后可能发生的data members的改变(比如在继承体系中上移或下移)所带来的冲击最小化了.假设把这些存取函数声明为 inline,就能够继续保持直接存取members的那种高效率--同一时候我们也兼顾了函数的封装性.此外,加法运算符不再须要被声明为Point的一个 friend.
然而,实际上并不能强迫将不论什么函数都变成 inline. 关键词 inline(或 class declaration中的member function或 friend function的定义) 仅仅是一项请求.假设这项请求被接受,编译器就必须觉得它能够用表达式合理地将这个函数扩展开来.
编译器相信它能够合理地扩展一个 inline 函数,意思是在某个层次上,其运行成本比一般的函数调用以及返回机制所带来的负荷低.cfront有一套复杂的測试法,通经常使用来计算assignments,function calls,virtual function calls等操作的次数.每一个表达式种类有一个权值,而 inline 函数的复杂度就以这些操作的总和来决定.
一般而言, 处理一个 inline 函数,有两个阶段:
1.分析函数定义,以决定函数的"intrinsic inline ability"(本质的 inline 能力)."intrinsic"(本质的,固有的)一词在这里意指"与编译器相关".
假设函数因其复杂度或建构问题,被推断为不可成为 inline,它会被转为一个 static 函数,并在"被编译模块"内产生相应的函数定义.
2.真正的 inline 函数扩展操作是在调用的那一点,这会带来參数的求值操作以及暂时性对象的管理.
相同是在扩展点上,编译器将决定这个调用是否"不可为inline".在cfront中,inline 函数假设仅仅有一个表达式,则其第二或后继的调用操作:
new_pt.x(lhs.x() + rhs.x());
就不会被扩展开来,这是由于在cfront中它被变成:
new_pt.x = lhs._x + x_5PointFv(&rhs);
这就全然没有带来效率上的改善!对此,唯一可以做的就是重写其内容:
new_pt.x(lhs._x + rhs._x);
形式參数 (Formal Arguments)
在 inline 扩展期间,究竟真正发生了什么事情?是的,每个形式參数都会被相应的实际參数代替.假设说有什么副作用,那就是不能够仅仅是简单地一一封塞程序中出现的每个形式參数,由于这将导致对于实际參数的多次求值操作. 一般而言,面对"会带来副作用的实际參数",通常都须要引入暂时性对象.换句话说, 假设实际參数是一个常量表达式,能够在替换之前先完毕求值操作;后继的 inline 替换,就能够把常量直接"绑"上去.假设既不是常量表达式,也不是带有副作用的表达式,那么就直接替换它.举个样例,如果有下面简单的 inline 函数:
inline int min(int i, int j) {
return i < j ? i : j;
}
以下是三个调用操作:
inline int bar() {
int minval;
int val1 = 1024;
int val2 = 2048;
/*1*/ minval = min(val1, val2);
/*2*/ minval = min(102, 2048);
/*3*/ minval = min(foo(), bar()+1);
return minval;
}
标示为1的那一行会被扩展为:// 參数直接替换
minval = val1 < val2 ? val1 : val2;
标示为2的那一行会被扩张为:// 替换后,直接使用常量
minval = 1024;
标示为3的那一行则引发參数的副作用,它须要导入一个暂时对象,以避免反复求值:// 有副作用,所以导入暂时对象
int t1;
int t2;
minval = (t1 = foo()), (t2 = bar() + 1), t1 < t2 ? t1 : t2;
局部变量 (Local Variables)
假设轻微地改变定义,在 inline 定义中增加一个局部变量,会如何:inline int min(int i, int j) {
int minval = i < j ? i : j;
return minval;
}
这个局部变量须要什么额外的支持或处理吗?假设有下面的调用操作:
{
int local_var;
int minval;
// ...
minval = min(va1, val2);
}
inline 被扩展后,为了维护其局部变量,可能会变成这样子(理论上这个样例中的局部变量能够被优化,其值能够直接在minval中计算):
{
int local_val;
int minval;
// 将inline函数的局部变量处以"mangling"操作
int __min_lv_minval;
minval = (__min_lv_minval = val1 < val2 ? val1 : val2), __min_lv_minval;
}
一般而言,
inline 函数中的每个局部变量都必须放在函数调用的一个封闭区段中,拥有一个独一无二的名称.假设 inline 函数以单一表达式扩展多次,那么每次扩展都须要自己的一组局部变量.假设 inline 函数以分离的多个式子被扩展多次,那么仅仅需一组局部变量,就能够反复使用.
inline 函数中的局部变量,再加上有副作用的參数,可能会导致大量暂时性对象的产生.特别是假设它以单一表达式被扩展多次的话.比如,以下的调用操作:
minval = min(val1, val2) + min(foo(), foo() + 1);
可能被扩展为:
// 为局部变量产生暂时变量
int __min_lv_minval_00;
int __min_lv_minval_01;
// 为放置副作用值而产生暂时变量
int t1;
int t2;
minval = ((__min_lv_minval_00 = val1 < val2 ? val1 : val2), __min_lv_minval_00) +
((__min_lv_minval_01 = (t1 = foo()), (t2 = foo() + 1), t1 < t2 ? t1 : t2), __min_lv_minval_01);
inline 函数对于封装提供了一种必须的支持,可能有效存取封装于 class 中的nonpublic数据.它同一时候也是C程序中大量使用的 #define (前置处理宏)的一个安全替代品--特别是假设宏中的參数有副作用的话,然而一个 inline 函数假设被调用太多次的话,会产生大量的扩张码,使程序的大小暴涨.
參数带有副作用,或是以一个单一表达式做多重调用,或是在 inline 函数中有多个局部变量, 都会产生暂时性对象,编译器或许可以把它们移除.此外,inline 中再有 inline,可能会使一个表面上看起来平庸的 inline 却因其连锁复杂度而没办法扩展开来.这样的情况可能发生于复杂度 class 体系下的constructors,或是object体系中一些表面上并不对的 inline 调用锁组成的串链--它们每个都会运行一小组运算,然后对还有一个对象发出请求.对于既要安全又要效率的程序,inline 函数提供了一个强而有力的工具.然而,与non-inline 函数比起来,它们须要更加小心地处理.