下面是 Point class 的一个加法运算符的可能实现内容:
class Point { friend Point opsrator+(const Point&, const Point&); }; Point opsrator+(const Point &lhs, const Point &rhs) { Point newPoint; newPt._x = lhs._x + rhs_x; newPt._y = lhs._y + rhs_y; }
理论上, 一个比较干净的做法是使用 inline 函数完成 set 和 get 函数:
//void Point::X(float x){_x = x;} //float Point::X() {return _x;} newPt.X()(lhs.X() + rhs.X());
由于我们受限于只能在上述两个函数中对 _x 直接存取, 因此也就将稍后可能发生的 data membrs 的改变(;例如在继承体系中的上移下移) 所带来的冲击最小化了. 如果把这些函数声明为 inline, 我们就可以继续保持直接存取 memebers 的那种高效率--亦兼顾了函数的封装性. 此外, 加法运算符不再需要被声明为 friend.
然而, 实际上我们不能把任何函数都声明为 inline--虽然 cfront 客户曾要求加上一个 must_inline 关键词. 关键词 inline(或 class declaration 中的 member function 或 friend function 的定义) 只是一项请求, 如果这项请求被接受, 编译器就必须认为它可以用一个表达式将这个函数合理的扩展开来.
"编译器相信他可以合理的扩展一个 inline 函数" 时, 我的意思是在某个层面上, 其执行成本比一般的函数调用及返回机制所带来的负荷低.
一般而言, 处理一个 inline 函数, 有两个阶段:
1. 分析函数定义, 以决定函数的 intrinsic inline ability(本质的 inline 能力). "intrinsic"(本质的, 固有的) 在这里指与编译其相关.
如果函数因复杂度, 或因其建构问题, 被判断不可成为 inline, 它会被转为一个 static 函数, 并在被编译模块内产生对应的函数定义. 在一个支持模块个别编译的环境中, 编译器几乎没有什么权宜之计, 理想情况下, 链接器会将被产生出来的重复东西清理掉, 然而一般来说, 目前市场上的连接器并不会将随该调用而被产生出来的重复调试信息清理掉. UNIX 环境中的 strip 命令可以达到这个目的. 我用的编译器为 VC++ 12.0 所以其连接器应该有此功能.
2. 真正的 inline 函数扩展操作是在调用的那个点上, 这会带来参数的求值操作, 以及临时性对象的管理.
同样是在扩展点上, 编译器将决定这个调用是否不可为 inline. 在 cfront 中, inline 函数如果只有一个表达式, 则其第二或后继的调用操作:
newPt.X(lhs.X() + rhs.X());
就不会扩展开来, 这是因为在 cfront 中, 它被变成:
//虚拟 C++ 码, 建议的 inline 扩展形式 newPt._x = lhs._x + x__5PointFV( &rhs );
这就完全没有效率上的改善! 对此, 我们能做的就是重写其内容:
//还是回来吧 newPt.X(lhs._x + rhs._x);
那么其他编一起在处理 inline 时, 有像 cfront 这样的束缚吗? 这个...大部分的编译器厂商似乎认为不值得在 inline 支持技术上做详细的讨论, 通常只有进入到汇编器才能看到是否实现了 inline.
形式参数(Formal Arguments)
在 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; minVal = Min(val1, val2); //1) minVal = Min(1024, 2048); //2) minVal = Min(Foo(), Bar() + 1); //3) return minVal; }
1) 会被扩展为:
minVal = val1 < val2 ? val1 : val2;
2) 直接拥抱常量:
minVal = 1024;
3)则引发参数的副作用, 它需要导入一个临时对象, 以避免重复求值:
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 localVal; int minVal; //... minVal = Min(val1, val2); }
inline 被展开后, 为了维护其局部变量, 可能会成为这个样子(理论上这和例子中的局部变量可以被优化, 其值可以直接在 Min() 中计算):
{ int loaclVar; int minVal; //将 inline 函数的局部变量处以 mangling 操作, 碾碎它们! int__min_lv_minVal; minVal = (__min_lv_minVal = val1 < val2 ? val1 : val2 ), __min_lv_minVal; }
一般而言, inline 函数中的每一个局部变量都必须放在函数调用的一个封闭区段中. 拥有一个独一无二的名称. 如果 inline 函数以单一表达式扩展多次, 那么每次扩展都需要自己的一组局部变量. 如果 inline 函数以分离的多个式子被扩展多次, 那么只需要一组变量, 就可以重复使用(因为他们被放在一个封闭区段中, 有自己的 scope).
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 中的 non-public 数据. 它同时也是 C 程序中大量使用的 #define 的一个安全替代品--特别是如果宏中的参数有副作用的话. 然而一个 inline 函数如果被调用太多次的话, 会产生大量的扩展码. 使程序的大小暴涨.
一如我所描述的, 参数带有副作用, 或是以一个单一表达式做多重调用, 或是在 inline函数中有多个局部变量, 都会产生临时性变量, 编译器也许(也许不) 能够把它们移出. 此外, inline 中 再有 inline, 可能会使一个表面上看起来平凡的 inline 却因为复杂度而没办法扩展开来. 这种情况可能发生于复杂 class 体系下的 constructors, 或是 object 体系中一些表面并不正确的 inline 调用所组成的串链--它们每一个都会执行一小组运算, 然后对另一个对象发出请求. 对于既要安全又要效率的程序, inline 函数提供了一个强有力的工具, 然而, 与 non-inline 函数比起来, 它们需要更小心处理.