第4章 Function 语意学3: 函数效能、Member Functions、inline
一、函数的效能
二、指向member function的指针
non-static指针
取一个 nonstatic member function 的地址,如果该函数是 nonvirtual,则得到的结果是它在内存中真正的地址。然而这个值需要被绑定于某个 class object 的地址上,才能够通过它调用该函数。所有的 nonstatic memberfunctions都需要对象的地址 (以参数 this 指出).
回顾一下,一个指向 member function 的指针,其声明语法如下
double ( Point::*pmf )();
然后我们可以这样定义并初始化该指针:
double (Point::*coord)() = &Point::x;
或
coord = &Point::y;
想调用它,可以这么做:
( origin.*coord )();
或
( ptr->*coord )();
这些操作会被编译器转化为:
//虚拟 C++码
( coord )( & origin );
//虚拟 C++ 码
( coord )( ptr );
指向 member function 的指针的声明语法,以及指向“member selection 运算符”的指针,是为了可以保留 this 指针的空间。
支持“指向 VirtualMember Functions”之指针
在前一小节中,我们看到了,对一个 nonstatic member function 取其地址,将获得该函数在内存中的地址。
然而,对一个 virtual member function 取其地址,所能获得的仅是 virtual function 在其相关之 virtual table中的索引值,因为其地址在编译时期是未知的,需要动态地去评估。
例如,假设我们有以下的 Point 声明
class Point{
public:
virtual ~Point();
float x();
float y();
virtual float z();//...
}
float (Point;:*pmf)() = &Point::z;
Point *ptr = new Point3d;
然后取 destructor 的地址,得到的结果是 1。取 x()或 y()的地址,得到的则是函数在内存中的地址,因为它们不是 virtual。取 z()的地址得到的结果是 2。
通过 pmf 来调用 z(),会被内部转化为如下:
( * ptr->vptr[ (int)pmf ])( ptr );
对一个“指向 member function 的指针”会有两种意义:
-
函数实际地址
-
虚函数在虚表中的下标
那么如何判断当前的函数指针是哪种含义呢?一种朴素的方式可以进行如下判断,如果当前值大于128,说明是地址,否则说明是下标:
((( int ) pmf ) & ~127 )
? // non-virtual invocation
(*pmf )( ptr )
: // virtual invocation
( * ptr->vptr[ (int) pmf ]( ptr ) );
在多重继承之下,指向Member Functions 的指针
为了让指向 member functions 的指针也能够支持多重继承和虚拟继承,Stroustrup设计了下面一个结构体:
//一般结构,用以支持
//在多重继承之下指向 member functions 的指针
struct _mptr {
int delta;
int index;
union {
ptrtofunc faddr;
int v_offset;
};
};
index 代表 virtual table 中的索引(当 index 不指向 virtual table 时、会被设为-1),faddr 代表 nonvirtual member function 地址。在该模型之下,调用操作会发生如下转变:
( ptr->*pmf )();
(pmf.index < 0 )
? ( *pmf.faddr )( ptr ) // non-virtual invocation
: ( * ptr->vptr[ pmf.index] (ptr) ); // virtual invocation
delta 字段表示 this 指针的 offset 值,而 v_offset 字段放的是一个 virtual(或多重继承中的第二或后继的)base class 的 vptr位置。此delta字段用于调整 this 指针,指向放置于 virtual table 中的适当地址。所有像这样的调用形式转换:
ptr->virt_func();
// 调用 virtual function
(*ptr->__vptr[ index ].addr)
(ptr + ptr->__vptr[ index ].delta ) // 将 this 指针的调整值传递过去
在这种实现技术之下不论是单一继承或多重继承,只要是虚拟调用操作,就会消耗相同的成本。
三、inline functions
inline 函数的执行成本更低,因为它通过函数拓展,消除了一般的函数调用及返回机制所带来的开销。
一般而言,处理一个 inline 函数,有两个阶段:
1.编译器分析函数定义,以决定函数是否可以支持inline;
2.调用上进行拓展,需要进行以下两步。
-
参数的求值操作 (evaluation):每一个形式参数都会被对应的实际参数取代
-
临时性对象的管理
举个例子,假设我们有以下的简单 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( vall, val2 );
/* (2) */minval = min( 1024,2048 );
/* (3) */minval = min( foo(), bar()+1 );
return minval;
}
标示为 (1)的那一行会被扩展为:
// (1)参数直接代换
minval = vall < val2 ? val1 : val2;
标示为(2)的那一行直接拥抱常量
// (2) 代换之后,直接使用常量
minval = 1024;
标示为(3)的那一行则引发参数的副作用。它需要导人一个临时对象,以避免重复求值 (multiple evaluations):
// (3)有副作用,所以导人临时对象
int tl;
int t2;
minval =
( t1 = foo() ),( t2 = bar() + 1 ),
tl < t2 ? t1 : t2;
局部变量
如果在 inline 定义中加一个局部变量,并进行以下调用
inline int
min( int i, int j ){
int minval = i < j ? i :j; return minval;
}
int local var;
int minval;
//...
minval = min( val1, val2 )
inline 被扩展开来后,为了维护其局部变量,可能会成为这个样子 (理论上这个例子中的局部变量可以被优化,其值可以直接在 minval 中计算):
int local_var;
int minval;
//将 inline 函数的局部变量处以“mangling”操作
int min_lv_minval;
minval =
(min_lv_minval = vall < val2 ? vall : val2 ),
min_lv_minval);