Function语义学——指向Member Function的指针

前言

 
在第三章中我们已经看出,取一个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
4
double        //return type
( Point::* //class the function is member
pmf) //name of the pointer
() //argument list

member function指针使用如下:
1
2
3
4
double (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
2
float (Point::*pmf)() = &Point::z;
Point *ptr = new Point3d;

显然,pmf指向了Point的一个member function,只不过这一次z()是一个virtual function。ptr指向了一个Point3d对象。
接下来我们执行调用操作:
1
2
ptr->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
8
class 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需要有两种含义:

  1. 内存地址
  2. 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
8
struct _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));

这种方法有两个缺点:

  1. 每一个调用操作都必须付出上述成本,检查其是否为virtual或non-virtual。
  2. 当传递一个不变值的指针给member function时,它需要产生一个临时对象。
    1
    2
    3
    4
    5
    extern Point3d foo(const Point3d&,Point3d(Point3d::*)());
    void bar(const Point3d& p){
    Point3d pt = foo(p,&Point3d::normal);
    ...
    }
    假设,&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的指针”的效率

 
(下文详细测试了不同指针的读取效率。)