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 函数比起来,它们需要更加小心地处理.