使用派生类对象通过成员函数指针调用基类虚函数之不可能性的证明

希望大家没有被这么拗口的标题吓到:)。 本文源于论坛上的这个问题:

struct base
{ 
	void foo() 
	{ 
		cout << "base::foo" << endl; 
	} 
	virtual void bar() 
	{ 
		cout << "base::bar" << endl; 
	} 
}; 

struct derived : base 
{ 
	virtual void bar() 
	{ 
		cout << "derived::bar" << endl; 
	} 
}; 

int __cdecl _tmain( int argc, _TCHAR* argv[] ) 
{ 
	void (base::*pfn)() = &base::bar; 
	derived d; 
	d.base::bar(); // 1 
	(d.base::*pfn)(); // 2 想实现和上一行一样的输出, 但是编译失败 
	return 0; 
}

很明显,标2的那一行是想使用派生类对象通过成员函数指针调用基类虚函数,以实现与标1的那行相同的输出,但却无法编译通过。这是个语法错误,因为 :: 运算符的优先级高于 .,所以那一行会先计算 base::*pfn,然而 *pfn 并不是 base 的成员,故有错误时很自然的。那么是否可以通过修改那条语句来达到目的呢?分析了成员函数指针的实现后,我发现,至少在 VC7.1 和 VC8 上,这是不可能的。由于 VC 的标准兼容性已经非常高,所以我怀疑 C++ 标准就不支持这种调用,但没有证实。

在上面的例子中,pfn 指向的是虚函数 bar,但它也必须能指向普通成员函数 foo。当指向 foo 时,它保存的就是 foo 的入口地址;然而当指向 bar 时,直接保存这个地址就不行了,因为对 basederived 来说,这个地址并不相同。VC 对此的解决方法是由编译器加入了一系列的内部函数 vcall。一个类中的每个虚函数都有一个唯一与之对应的 vcall 函数,但在不同类之间,这些 vcall 实际上是公用的。pfn 指向的就是这些 vcall 中的一个。

我们知道,调用成员函数时要传递 this 指针。一般情况下,它是通过 ecx 寄存器传递的,所以 vcall 的实现如下所示:

mov eax, dword ptr[ecx]; 
jmp dword ptr [eax+xx]

第一句中,由于 ecxthis 指针,而一般 vfptr 是类的第一个成员,所以它是把 vfptr,也就是 vtable 的地址存到了 eax 中。第二句里面的 xx,在32位计算机上,是 4 的整数倍,所以这一句的意思是:跳转到 vtable 的第 xx/4 项所指的地址上,这个地址就是最终要调用的函数的入口。

明白了虚成员函数指针的实现,就可以看那种调用为什么不能实现了。让我们用反证法,如果它能实现,为讨论方便就假设标 2 的那一句是正确的吧,在进行调用时,编译器首先要保证传递的是指向 dthis 指针,然后还要保证 this 所指向的 vfptr 所指的是 basevtable。编译器能做到吗?当然能,它可以在调用前偷偷修改 vfptr 使其指向 basevtable,并在调用返回后再把它恢复过来,但想想这样做在多线程环境中的后果吧。所以编译器是不可能这样做的,也就是说“使用派生类对象通过成员函数指针调用基类虚函数是不可能的”。