4.4 指向member function的指针
取一个 nonstatic member function 地址,如果函数是 nonvirtual,得到的是他在内存中的真正地址,同时,他还需要绑定于某个class object的地址上,才能调用该函数。所有的 nonstatic member functions都需要对象的地址。(以参数this指出)
函数指针的声明:
一个指向 member function 的指针,其声明:
double (Point::*pmf)();
double (Point::*coord)() = &Point::x; // x()
(origin.*coord)(); // 调用
(ptr->*coord)(); // 调用
// 被转换为:
(coord)(&origin);
(coord)(ptr);
支持 “指向 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() 被调用吗? 也就是说, 虚拟机制仍然能够在使用指向 member function 之指针的情况下运行吗? 答案是肯定的, 问题这是咋实现的呢?
Ⅰ、对一个 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();
//...
};
取 X() 或 Y() 的地址:
&Point::X();
&Point::Y();
得到的则是函数在内存中的地址, 因为它们不是 virtual。
取 z() 的地址:
&Point::Z();
得到的结果是索引 2. 通过 pmf 来调用 Z(), 会被内部转化为一个编译时期的式子:
( *ptr->vptr[ (int)pmf ] ) (ptr);
对一个指向 member function 的指针评估求值, 会因为该值有两种意义而复杂化; 其调用操作也将有别于常规操作。
float (Point::*pmf)(); // pmf 的内部定义
必须允许该函数能够寻址出 novirtual x() 和 virtual z() 两个 member functions, 而那两个函数有着相同的原型:
//二者都可以被指定给 pmf
float Point::X() { return _x; }
float Point::Z() { return 0; }
只不过其中一个代表内存地址, 另一个代表 virtual table 中的索引值. 因此编译器必须定义 pmf 使它能够:
- 含有两种数值
- 更重要的是其数值可以被区分代表内存的的地址还是 virtual table 中的索引值
在 cfront 2.0 非正式版中, 这两个值被内含在一个普通的指针内. cfront 如何识别该值是内存地址还是 virtual table 中的 slot 呢? 使用了如下技巧:
( ((int)pmf) & ~127) ? (*pmf)(ptr) : (*ptr->vptr[ (int) pmf ](ptr ) );
//nonvirtual invocation
//virtual invocation
当然, 这种实现技巧必须假设继承体系中最多只有 128 个 virtual functions. 这并不是开始所希望的, 但却证明是可行的. 然而多重继承的引入, 导致需要更一般化的实现方式, 并趁机除去对 virtual functions 的数目限制.
在多重继承下,指向 Member Function 的指针
为了让指向 member functions 的指针也能够支持多重继承和虚拟继承, Stroustrup 设计了下面一个结构体:
struct __mptr
{
int delta;
int index;
union
{
ptrtofun faddr;
int v_offset;
};
};
这是想表达什么呢? index 和 faddr 分别带有 virtual table 索引和 nonvirtual member function 地址(为了方便, 当index 不指向 virtual table 时, 会被设为 -1). 在该模型之下, 像这样的调用操作:
(ptr->*pmf)();
//会变成
(pmf.index < 0) ? (*pmf.faddr)(ptr) : (*ptr->vptr[ pmf.index] (ptr));
// nonvirtual invocation
// virtual invocation
这种方法所受的批评是, 每一个调用操作都得检查其是否为virtual 或 nonvirtual。 Microsoft 把这项检查拿掉, 导入一个所谓的 vcall thunk。在此策略之下, faddr 指定的要么是真正的 member function 地址, 要么是 vcall thunk 地址。于是 virtual 或 nonvirtual 函数的调用操作透明化, vcall thunk 会选出并调用相关 virtual table 中适当的 slot。
这个结构体的另一个副作用就是, 当传递一个不变值的指针给 member function 时, 它需要产生一个临时性对象, 就是说, 如果你这么做:
extern Point3d Foo(cosnt Point3d&, Point3d(Point3d::*)());
void Bar(const Point3d &p)
{
Point3d pt = foo(p, &Point3d::normal);
//其中 &Point3d::normal 的值类似这样: {0, -1, 10727417}
}
这将需要产生一个临时对象, 有明确的初值:
//虚拟 C++ 代码
__mptr temp = {0, -1, 1027417}
foo(p, temp);
回到一开始的那个结构体, delte 表示 this 指针的 offset 值, 而 v_offset 放置的是一个virtual(或多重继承中的第二或后继的) base class 的 vptr 位置. 如果 vptr 被编译器放在 class 对象的起头处, 这个字段就没有必要了, 代价则是 C 对象的兼容性降低. 这些字段值在多重继承或虚拟继承的情况下才有必要性, 有许多编译器在自身内部根据不同的 classes 特性提供多种指向 member functions 的指针形式. 例如Microsoft 就供应了三种风味:
1, 一个单一继承实例(其中带有 vcall thunk 地址或是函数地址);
2. 一个多重继承实例(其中带有 faddr 和 delta 两个 members);
3. 一个虚拟继承实例(其中带有四个 members).
指向 Member Functions 指针 的效率
对于指向 Member Functions 的指针, 多数编译器中都会把以下函数调用:
(pA.*pmf)(pB);
转化为:
pmf.index < 0 ? (*pmf.faddr)(&pA + pmf.delta, pB): (*pA.__vptr__Point3d[pmf.index].delta, pB);
其效率在未优化时最高的是 nonmember function 的指针, 多重继承 且 nonvirtual 与 Member nonvirtual function 的效率不相上下, 在优化后前三种指针效率相同. 虚拟继承 且 nonvirtual 优化前的效率略高于 virtual member function, 优化后效率接近, 在不同的编译器上有不同的表现, 即前者的效率不低于后者. 多重继承 且 virtual 毫无疑问是效率最低的, 当你的设计中出现了这种 member function 的指针, 可能你已踏入复杂和无法确定行为的深渊。

浙公网安备 33010602011771号