C++对象模型——指向Member Function的指针 (Pointer-to-Member Functions)(第四章)

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 指针调整.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值