4.4 指向Member Function的指针 (Pointer-to-Member Functions)
取一个nonstatic data member的地址,得到的结果是该member在 class 布局中的byte位置(再加1),它是一个不完整的值,须要被绑定于某个 class object的地址上,才可以被存取.
取一个nonstatic member function的地址,假设该函数是nonvirtual,则得到的结果是它在内存中真正的地址.然而这个值也是不全然的,它也须要被绑定与某个 class object的地址上,才可以通过它调用该函数,全部的nonstatic member functions都须要对象的地址(以參数 this 指出).
一个指向member function的指针,其声明语法例如以下:
double // return type
(Point::* // class the function is member
pmf) // name of the pointer to member
(); // argument list
然后能够这样定义并初始化该指针:
double (Point::*coord)() = &Point::x;
也能够这样指定值:
coord = &Point::y;
想调用它,能够这样做:
(origin.*coord)();
或
(ptr->*coord)();
这些操作会被编译器转化为:
(coord)(&origin);
和
(coord)(ptr);
指向member function的指针的声明语法,以及指向"member selection运算符"的指针,其作用是作为 this 指针的空间保留者.这这也就是为什么 static member function(没有 this
指针)的类型是"函数指针",而不是"指向member function的指针"的原因.
使用一个"member function指针",假设并不用于 virtual function,多重继承,virtual base class 等情况的话,并不会比使用一个"nonmember function指针"的成本更高.上述三种情况对于"member function指针"的类型以及调用都太过复杂.其实,对于那些没有 virtual functions或 virtual base class,或multiple
base class 的 class 而言,编译器能够为它们提供同样的效率.下一节讨论为什么 virtual function的出现,会使得"member function指针"更复杂化.
支持"指向Virtual Member Functions"的指针
注意以下的程序片段:
float (Point::*pmf)() = &Point::z;
Point *ptr = new Point3d;
pmf,一个指向member function的指针,被设值为Point::z()(一个 virtual function)的地址,ptr则被指定以一个Point3d对象,假设直接经由ptr调用z():
ptr->z();
则被调用的是point3d::z(),但假设从pmf间接调用z()呢?
(ptr->pmf)();
仍然是Point3d::z()被调用吗?也就是说,虚拟机制仍然可以在使用"指向member function的指针"的情况下运行吗?答案是yes,问题是怎样实现呢?
对一个nonstatic member function取其地址,将获得该函数在内存中的地址,然而面对一个 virtual function,其地址在编译时期是未知的,所能直到的仅是 virtual function在其相关的 virtual table中的索引值.也就是说,对一个 virtual member function取其地址,所能获得的仅仅是一个索引值.
比如,如果有下面的Point声明:
class Point {
public:
virtual ~Point();
float x();
float y();
virtual float z();
};
然而取得destructor的地址:
&Point::~Point;
得到的结果是1,取x()或y()的地址:
&Point::x();
&Point::y();
得到的则是函数在内存中的地址,由于它们不是 virtual,取z()的地址:
&Point::z();
得到的结果是2,通过pmf来调用z(),会被内部转化为一个编译时期的式子,一般形式例如以下:
(*ptr->vptr[(int)pmf])(ptr);
对一个"指向member function的指针"评估求值(evaluted),会由于该值有两种意义而复杂化;其调用操作也将有别于常规调用操作.pmf的内部定义,也就是:
float (Point::*pmf)();
必须同意该函数可以寻址出nonvirtual x()和 virtual z()两个member functions,而那两个函数有着同样的原型:
// 二者都能够被指定给pmf
float Point::x() { return _x; }
float Point::z() { return 0; }
仅仅只是当中一个代表内存地址,还有一个代表 virtual table中的索引值.因此,编译器必须定义pmf使它能够(1)还有两种数值,(2)更重要的是其数值能够被差别代表内存地址还是 virtual table中的索引值.
在多重继承下,指向Member Functions的指针
为了让指向member functions的指针也可以支持多重继承和虚拟继承,Stroustrup设计了以下一个结构体:
// 一般结构,用以支持在多重继承下指向member functions的指针
struct __mptr {
int delta;
int index;
union {
ptrtofunc faddr;
int v_offset;
};
};
它们要表现什么呢?index和faddr分别(不同一时候)带有 virtual table索引和nonvirtual member function地址.在该模型下,像这种调用操作:
(ptr->*pmf)();
会变成:
(pmf.index < 0)
? // non-virtual invocation
(*pmf.faddr)(ptr)
: // virtual invocation
(*ptr->vptr[pmf.index](ptr));
这样的方法所受到的批评是,每个调用操作都得付出上述成本,检查其是否为 virtual 或 nonvirtual.Microsoft把这项检查拿掉,导入一个它所谓的vcall thunk.在此策略下,faddr被指定的要不就是真正的member function地址(假设函数是nonvirtual的话),要不就是vcall thunk的地址.于是 virtual 或novirtual函数调用操作透明化,vcall
thunk会选出并调用相关 virtual table 中的适当slot.
这个结构体的还有一个副作用是,当传递一个不变的值的指针给member function时,它须要产生一个暂时性对象.举个样例,假设这样做:
extern Point3d foo(const Point3ed&, Point3d(Point3d::*)());
void bar(const Point3d& p) {
Point3d pt = foo(p, &Point3d::normal);
}
当中&Point3d::normal的值类似这样:
{0, -1, 10727417}
将须要产生一个暂时性对象,有明白的初值:
__mptr temp = {0, -1, 10727417}
foo(p, temp);
本节一開始的那个结构体,delta字段表示 this 指针的offset值.而v_offset字段放的是一个 virtual(或多重继承中的第二或后继的)base class 的vptr位置.假设vptr被编译器放在 class 对象的起始处,这个字段就没有必要了,代价则是C对象兼容性减少.这些字段仅仅在多重继承或虚拟继承的情况下才有其必要性,有很多编译器在自身内部依据不同的 class 特性提供多种指向member
functions的指针形式,比如Microsoft就供应了三种风格:
1. 一个单一继承实例(当中带有vcall thunk地址或是函数地址)
2. 一个多重继承实例(当中带有faddr和delta两个members)
3. 一个虚拟继承实例(当中带有四个members)
"指向Member Functions的指针"的效率
在以下一组測试中,cross_product()函数经由以下方式调用:
1. 一个指向 nonmember function 的指针
2. 一个指向 class member function 的指针
3. 一个指向 virtual member function 的指针
4. 多重继承下的 nonvirtual 以及 virtual member function call
5. 虚拟继承下的 nonvirtual 以及 virtual member function call.
第一个測试(指向 nonmember function 的指针)下面列方式进行:
Point3d* (*pf)(const Point3d&, const Point3d &) = cross_product;
for (int iters = 0; iters < 10000000; iters++)
(*pf)(pA, pB);
return 0;
第二个測试(指向 member function 的指针)的声明和调用操作例如以下:
Point3d* (Point3d::*pmf)(const Point3d &) const = &Point3d::cross_product;
for (int iters = 0; iters < 10000000; iters++)
(pA.*pmf)(pB);
上述操作会被转化为下面的内部形式,于是下面的函数调用:
(pA.*pmf)(pB);
会被转化为这种推断:
pmf.index < 0
? (*pmf.faddr)(&pA + pmf.delta, pB)
: (*pA.__vptr__Point3d[pmf.index].faddr)
(&pA + pA.__vptr__Point3d[pmf.index].delta, pB);
一个"指向member function的指针"是一个结构,内含三个字段:index,faddr和delta.index若不是内带一个相关 virtual table的索引值,就是以-1表示函数是 nonvirtual.faddr带有nonvirtual member function的地址.delta带有一个可能的 this 指针调整.