前言
在第三章中我们已经看出,取一个nonstatic data member的地址,得到的结果是该member在class布局中的bytes位置(+1)。我们可以想象它是一个不完整的值,必须依附于某个class object才可以完成存取。
Nostatic Nonvirtual Member Function的指针
取一个nonstatic nonvirtual member function的地址,得到的是该函数在内存中真正的地址,但这个值也是不完全的,它也需要被绑定到某个class object的地址上才能调用该函数。所有nonstatic member function都需要对象的地址(以参数this指出)。
一个指向member function的指针其声明语法如下:1
2
3
4double //return type
( Point::* //class the function is member
pmf) //name of the pointer
() //argument list
member function指针使用如下:1
2
3
4double (Point::*coord)() = &Point::x;//定义、初始化
coord = &Point::y;//赋值
(origin.*coord)();//调用 被编译器转为(coord)(&origin)
(ptr->*coord)();//调用 被编译器转为(coord)(ptr)
指向member function的指针的声明语法,以及指向“member selection运算符”的指针,其作用是作为this指针的空间保留者。这也是static member functions的类型是函数指针,而非“指向member function的指针”的原因。
我们可以认为,使用“指向member function的指针”,其成本并不高于使用一个“nonmember function指针”,这句话的前提是,当前并没有发生virtual function、多重继承、虚继承。
支持“指向Virtual Member Functions的指针”
考虑以下程序片段:1
2float (Point::*pmf)() = &Point::z;
Point *ptr = new Point3d;
显然,pmf指向了Point的一个member function,只不过这一次z()是一个virtual function。ptr指向了一个Point3d对象。
接下来我们执行调用操作:1
2ptr->z();//正确地调用Point3d::z()
(ptr->*pmf)();//仍然具备正确性
Poninter to virtual Member Function的实现
在上文中我们已经明确,对一个nonstatic member function取址得到的是其在内存中的地址。但对于一个virtual function,其地址在编译期是未知的,我们所知道的只是virtual function在其相关vtbl中的索引值。因此,对一个virtual member function取址的时候,我们得到的是一个索引。
问题实例
现有Point声明如下:1
2
3
4
5
6
7
8class Point{
public:
virtual ~Point();
float x();
float y();
virtual float z();
...
};
现对各个member func分别取址:1
2
3
4&Point::~Point();//res==1;
&Point::x();//内存中地址
&Point::y();//内存中地址
&Point::z()//res==2
通过pmf来调用z()的话,编译器会将该调用语句转为:1
(*ptr->vptr[(int)pmf])(ptr);
显然地,pmf需要有两种含义:
- 内存地址
- vtbl中的索引值
关键在于我们应当如何区分它们。
解决方案
在cfront编译器中,具体解决方案如下:1
2
3(((int)pmf)&~127)?
(*pmf)(ptr):
(*ptr->vptr[(int)pmf](ptr));
这种设计策略要求继承体系中最多只能存在128个virtual functions。由于多重继承的导入,我们需要更一般化的实现方式,并趁机去除virtual functions的数目限制。
多重继承下,指向member function的指针
为了保证支持多重继承与虚继承,cfront的作者设计了一个结构体:1
2
3
4
5
6
7
8struct _mptr{
int delta;
int index;
union{
ptrtofunc faddr;
int v_offset;
};
};
index与faddr分别(不同时)带有vbtl索引与nonvirtual member function地址。在该模型下,调用操作1
(ptr->*pmf)();
会变成:1
2
3(pmf.index<0)
?(*pmf.faddr)(ptr)//non-virtual
:(*ptr->vptr[pmf.index](ptr));
这种方法有两个缺点:
- 每一个调用操作都必须付出上述成本,检查其是否为virtual或non-virtual。
- 当传递一个不变值的指针给member function时,它需要产生一个临时对象。假设,&Point3d::normal的值类似于:
1
2
3
4
5extern Point3d foo(const Point3d&,Point3d(Point3d::*)());
void bar(const Point3d& p){
Point3d pt = foo(p,&Point3d::normal);
...
}{0,-1,10727417}
,那么将会产生一个具有明确初值的临时对象:1
2_mptr temp={0,-1,10727417};
foo(p,temp);
现在我们将目光再次拉回结构体。delta字段表示this指针的offset值,而v_offset字段存放的则是一个virtual base class的vptr位置。显然,如果vptr被置于class object的起始处,我们就可以不需要这个字段(当然这令我们丧失了与C语言的兼容性)。这些字段只有在多重继承或者虚拟继承的情况下才具备必要性。
“指向Member Functions的指针”的效率
(下文详细测试了不同指针的读取效率。)