本文接前面"Be Careful with Pointer to Member Function",对成员函数指针做了一个更深入的探讨,本文的内容来源于个人对VS生成的一些汇编码以及在Jan Gray, "C++: Under the Hood"文中一些内容的理解上而推出的一些结论,其中可能存在一些不正确的结论,如果读者对相关内容有更深入的理解,很感激能告诉我。
对于成员函数期望的this指针的类型
对于非多态函数,他期望的this指针是当前定义该函数的类型的指针
在继承情况的时候,如果基类定义了这个函数,但是在子类没有定义这个函数,则这个函数期望的是基类的指针
如果子类重写定义了这个函数,那么这个函数期望的将是子类的指针。
class A
{
public:
int a;
};
class B
{
public:
void funcB(){}
int b;
};
class C : public A, public B
{
public:
void funcB(){}
int c;
};
这里C::funcB期望的是C*, B::funcB期望的是B*,如果C没有定义funcB,则C::funcB期望的是B*。
对于多态函数,他期望的this指针是最先引入该多态函数的类型的指针
比如
class Base
{
public:
virtual void func(){}
};
class Dummy
{
public:
virtual void dummy(){}
};
class Derive : public Dummy, public Base
{
public:
virtual void func(){}
};
这里Derive重写了Base中的func(),但是func期望的this仍然是Base*,所以在使用Derive*调用func的时候,实际上会将Derive*转化为Base*来调用(也就是在调用之前有一个offset施加)。
但是对于这种情况
class Base
{
public:
virtual void func(){}
};
class Dummy
{
public:
virtual void func(){}
};
class Derive : public Dummy, public Base
{
public:
virtual void func(){}
};
Derive重写了Base和Dummy中的func,因为在继承声明中,对于func最先导入的是Dummy类,所以在Derive中的func期望的是Dummy*(这里也就是Derive*,但是,如果Dummy前面还有一个基类Base0,并且Base0也有虚函数(如果没有虚函数,编译器会做优化,把Base0调到所有具有虚函数基类的后面去),这个时候Dummy*就不是Derive*了,注意这一点),而对于Base下来的func实际上仍然期望的是Base*,但是在Derive中对于Base的虚表中最后要调用的是Derive::func,而Derive::func期望的是Dummy*,这个时候Base*就需要转化为Dummy*,而这个转化过程在一个thunk中完成,thunk中将Base*转化为Dummy*然后跳转到Derive::func中,这被称为adjust-and-jump技术。
其实也就是说,Derive中对于Dummy的func和Base的func仍然期望的是原始类型的this,但是在Derive中的func期望的是在继承中最先声明的基类的this,而对于后面的基类,则引入thunk来进行修正。因此最后在Derive::Dummy虚表中直接记录的是Derive::func,在Derive::Base虚表中记录的是一个thunk。
如下面的图所示
thunk什么时候才会被引入?当Derive类中的一个虚函数override了多个父类的虚函数,这个时候,在这些父类总对于不是最左边的父类的虚表中对应虚函数项保存的是一个thunk。仅当这个时候会引入thunk。
注意:对于虚继承部分,这里不讨论,那个有些复杂。
在上面,弄清了,一个成员函数他期望的this指针应该是什么类型,那么后面来讨论对于成员函数指针的问题。
typedef ret_type(ClassType::* MemFunc)(args);
在这个typedef中,MemFunc为一个成员函数指针类型。
MemFunc memFunc;
对于这样一个语句,对于编译器而言,他定义了对于MemFunc类型的成员函数,他所期望的this总是为ClassType*。
1:
那么对于任意一个对象或者对象指针obj,pObj.
(obj.*memFunc)();
(pObj->*memFunc)();
他们都会将&obj和pObj隐式转为ClassType*再进行后面的调用过程。如果&obj和pObj不能隐式转为ClassType*,那么编译器就会出错。
这个过程只完成了一步。
2:
对于具体的成员函数指针的赋值过程
memFunc = &ClassType::Func;
编译器在处理这个过程的时候,如前面所说Func自己有一个自己期望的this类型,假设记为Target*类型,而memFunc在1中得到的总是为ClassType*,所以这个时候需要保存一些额外的信息。
1) 如果Target*和ClassType*没有偏移施加,那么memFunc就直接保存&ClassType::Func的地址值,这个时候MemFunc的大小为4bytes。这通常是单继承的情况。
2) 如果Target*和ClassType*仅仅相差一个Offset,那么memFunc不仅要保存&ClassType::Func的地址值,还需要保存这个偏移值,这个时候MemFunc的大小为8bytes。4bytes保存funcPtr,4bytes保存offset。这通常是多继承的情况
3) 如果Target*和ClassType*具有虚拟继承的情况,那么这个时候,不仅仅是Offset和&ClassType::Func的地址,还需要一个在vbtable上的偏移。这个时候MemFunc大小为12bytes。4bytes保存funcPtr,4bytes保存offset,4bytes保存vbtable上的偏移。这通常是在虚继承的情况
所以成员函数指针不能同等于一般的函数指针。
那么整个成员函数指针的调用过程为:
1:将obj或者pObj转为ClassType*作为当前的this。
2:通过memFunc中保存的信息对this施加偏移,得到memFunc本身所代表的成员函数所期望的类型的this。
3:跳转到memFunc保存的funcPtr。
例子:
class Dummy
{
public:
int d;
virtual void funcD(){}
};
class A
{
public:
virtual void vfunc(){}
int a;
};
class B
{
public:
virtual void vfunc(){}
int b;
};
class E : public Dummy, public A , public B
{
public:
virtual void vfunc(){}
int f;
};
int main()
{
E e;
typedef void(B::* BMemFunc)();
BMemFunc bMemFunc = &B::vfunc;
(e.*bMemFunc)();
return 0;
}
分析:
typedef void(B::* BMemFunc)(); 指定BMemFunc为期望B*
BMemFunc bMemFunc = &B::vfunc; B::vfunc期望为B*
所以bMemFunc不会保存偏移值
(e.*bMemFunc)(); 这里e会先得到B*,然后直接跳转到B::vfunc。
E::B::vftabel中保存的是一个thunk,所以在thunk中会将B*修正为A*,然后进入到E::vfunc.
汇编码:
BMemFunc bMemFunc = &B::vfunc;
00411476 mov dword ptr [bMemFunc],offset B::`vcall'{0}' (411249h) // No offset
(e.*bMemFunc)();
0041147D lea eax,[e]
00411480 test eax,eax
00411482 je main+42h (411492h)
00411484 lea ecx,[e]
00411487 add ecx,10h //Cast E* to B*
0041148A mov dword ptr [ebp-0F4h],ecx
00411490 jmp main+4Ch (41149Ch)
00411492 mov dword ptr [ebp-0F4h],0
0041149C mov esi,esp
0041149E mov ecx,dword ptr [ebp-0F4h]
004114A4 call dword ptr [bMemFunc]
00411249 jmp B::`vcall'{0}' (411590h)
B::`vcall'{0}':
00411590 mov eax,dword ptr [ecx]
00411592 jmp dword ptr [eax]
0041123F jmp [thunk]:E::vfunc`adjustor{8}' (411880h)
[thunk]:E::vfunc`adjustor{8}':
00411880 sub ecx,8 // Cast B* to A*
00411883 jmp E::vfunc (411127h) // Jump to E::vfunc
将上面的例子改为:
int main()
{
E e;
typedef void(E::* EMemFunc)();
EMemFunc eMemFunc = &B::vfunc;
(e.*eMemFunc)();
return 0;
}
分析:
typedef void(E::* EMemFunc)();表明EMemFunc总是期望为E*
EMemFunc eMemFunc = &B::vfunc;这个时候B::vfunc期望的是B*
所以这个时候需要一个偏移
汇编码:
EMemFunc eMemFunc = &B::vfunc;
00411476 mov dword ptr [ebp-100h],offset B::`vcall'{0}' (411249h) // Record the funcPtr
00411480 mov dword ptr [ebp-0FCh],10h // Record the offset
0041148A mov eax,dword ptr [ebp-100h]
00411490 mov dword ptr [eMemFunc],eax // Record the funcPtr
00411493 mov ecx,dword ptr [ebp-0FCh]
00411499 mov dword ptr [ebp-2Ch],ecx // Record the offset
(e.*eMemFunc)();
0041149C mov eax,dword ptr [ebp-2Ch] // Get the recorded offset
0041149F lea ecx,e[eax] // Do the offset in E*, and ecx stores B*
004114A3 mov esi,esp
004114A5 call dword ptr [eMemFunc] // The same as above
如果例子改为
int main()
{
E e;
typedef void(E::* EMemFunc)();
EMemFunc eMemFunc = &E::vfunc;
(e.*eMemFunc)();
return 0;
}
这个时候E::vfunc期望的是A*,所以变化的只有offset
汇编码:
EMemFunc eMemFunc = &E::vfunc;
00411476 mov dword ptr [ebp-100h],offset B::`vcall'{0}' (41124Eh)
00411480 mov dword ptr [ebp-0FCh],8 // Record the offset
0041148A mov eax,dword ptr [ebp-100h]
00411490 mov dword ptr [eMemFunc],eax
00411493 mov ecx,dword ptr [ebp-0FCh]
00411499 mov dword ptr [ebp-2Ch],ecx
(e.*eMemFunc)();
0041149C mov eax,dword ptr [ebp-2Ch]
0041149F lea ecx,e[eax]
004114A3 mov esi,esp
004114A5 call dword ptr [eMemFunc]
本文依赖的平台为VS2008