求救:C++类中的函数指针数组错误
想在类中建立一个函数指针数组(公有的),用它来索引一系列成员函数(私有的)
比如:
class tcmd
{
public:
tcmd()
{
cmd[0] = cmd_A;
}
void (*cmd[10])();
private:
void cmd_A() {}
};
但有问题: “error C2440: '=' : cannot convert from 'void (__thiscall Stock::*)(void)' to 'void (__cdecl *)(void)'
There is no context in which this conversion is possible”
不知犯了什么错误,求修正
如果想要实现该怎么弄 请教各位大神
[解决办法]
4.1 Microsoft的实现
4.1.1 内部表示
Microsoft VC的实现采用的是Microsoft一贯使用的Thunk技术(不知道这个名字怎么来的,不过有趣的是把它反过来拼写就变成了大牛Knuth的名字,呵呵)。
对于Mircosoft来说,成员函数指针实际上分两种,一种需要调节this指针,一种不需要调节this指针。
先分清楚那些情况下成员函数指针需要调整this指针,那些情况下不需要。回忆上一节讨论的c++对象内存布局的说明,我们可以得出结论如下:
如果一个类对象obj含有一些子对象subobj,这些子对象的首地址&subobj和对象自己的首地址&obj不等的话,就有可能需要调整this指针。因为我们有可能把subobj的函数当成obj自己的函数来使用。
根据这个原则,可以知道下列情况不需要调整this指针:
继承树最顶层的类。
单继承,若所有类都不含有虚拟函数,那么该继承树上所有类都不需要调整this指针。
单继承,若最顶层的类含有虚函数,那么该继承树上所有类都不需要调整this指针。
下列情况可能进行this指针调整:
多继承
单继承,最顶的base class不含virtual function,但继承类含虚函数。那么这些继承类可能需要进行this指针调整。
Microsoft把这两种情况分得很清楚。所以成员函数的内部表示大致分下面两种:
struct pmf_type1{
void* vcall_addr;
};
struct pmf_type2{
void* vcall_addr;
int delta; //调整this指针用
};
这两种表示导致成员函数指针的大小可能不一样,pmf_type1大小为4,pmf_type2大小为8。有兴趣的话可以写一段代码测试一下。
4.1.2 Vcall_addr实现
上面两个结构中出现了vcall_addr, 它就是Microsoft 的Thunk技术核心所在。简单的说,vcall_addr是一个指针,这个指针隐藏了它所指的函数是虚拟函数还是普通函数的区别。事实上,若它所指的成员函数是一个普通成员函数,那么这个地址也就是这个成员函数的函数地址。若是虚拟成员函数,那么这个指针指向一小段代码,这段代码会根据this指针和虚函数索引值寻找出真正的函数地址,然后跳转(注意是跳转jmp,而不是函数调用call)到真实的函数地址处执行。
看一个例子。
//源代码
class C
{
public:
int nv_fun1(int) {return 0;}
virtual int v_fun(int) {return 0;}
virtual int v_fun_2(int) {return 0;}
};
void foo(C *c)
{
int (C::*pmf)(int);
pmf = &C::nv_fun1;
(c->*pmf)(0x12345678);
pmf = &C::v_fun;
(c->*pmf)(0x87654321);
pmf = &C::v_fun_2;
(c->*pmf)(0x87654321);
}
; foo的汇编代码,release版本,部分地方进行了优化
:00401000 56 push esi
:00401001 8B742408 mov esi, dword ptr [esp+08]
; pmf = &C::nv_fun1;
; (c->*pmf)(0x12345678);
:00401005 6878563412 push 12345678
:0040100A 8BCE mov ecx, esi ;this
:0040100C E81F000000 call 00401030
; pmf = &C::v_fun;
; (c->*pmf)(0x87654321);
:00401011 6821436587 push 87654321
:00401016 8BCE mov ecx, esi ;this
:00401018 E803070000 call 00401720
; pmf = &C::v_fun_2;
; (c->*pmf)(0x87654321);
:0040101D 6821436587 push 87654321
:00401022 8BCE mov ecx, esi ;this
:00401024 E807070000 call 00401730
:00401029 5E pop esi
:0040102A C3 ret
:00401030 33C0 ; 函数实现 xor eax, eax
:00401032 C20400 ret 0004
:00401720 8B01 ; vcall mov eax, dword ptr [ecx]
:00401722 FF20 jmp dword ptr [eax]
:00401730 8B01 ; vcall mov eax, dword ptr [ecx]
:00401732 FF6004 jmp [eax+04]
从上面的汇编代码可以看出vcall_addr的用法。00401030, 00401720, 00401730都是vcall_addr的值,其实也就是pmf的值。在调用的地方,我们不能分别出是不是虚函数,所看到的都是一个函数地址。但是在vcall_addr被当成函数地址调用后,进入vcall_addr,就有区别了。00401720, 00401730是两个虚函数的vcall,他们都是先根据this指针,计算出函数地址,然后jmp到真正的函数地址。00401030是C::nv_fun1的真实地址。
Microsoft的这种实现需要对一个类的每个用到了的虚函数,都分别产生这样的一段代码。这就像一个template函数:
template <int index>
void vcall(void* this)
{
jmp this->vptr[index]; //pseudo asm code
}
每种不同的index都要产生一个实例。
Microsoft就是采用这样的方式实现了虚成员函数指针的调用。
4.1.3 This指针调整
不过还有一个this调整的问题,我们还没有解决。上面的例子为了简化,我们故意避开了this指针调整。不过有了上面的基础,我们再讨论this指针调整就容易了。
首先我们需要构造一个需要进行this指针调整的情况。回忆这节开头,我们讨论了哪些情况下需要进行this指针调整。我们用一个单继承的例子来进行说明。这次我们避开virtual/non-virtual function的问题暂不考虑。
class B {
public:
B():m_b(0x13572468){}
int b_fun(int) {
std::cout<<'B'<<std::endl;
return 0;
}
private:
int m_b;
};
class D : public B {
public:
D():m_d(0x24681357){}
virtual int foo(int) {
std::cout<<'D'<<std::endl;
return 0;
}
private:
int m_d;
};
// 注意这个例子中virtual的使用
void test_this_adjust(D *pd, int (D::*pmf)(int))
{
(pd->*pmf)(0x12345678);
}
:00401000 mov eax, dword ptr [esp+04] ; this入参
:00401004 mov ecx, dword ptr [esp+0C] ; delta入参
:00401008 push 12345678 ;参数入栈
:0040100D add ecx, eax ; this = ecx= this+delta
:0040100F call [esp+0C] ; vcall_addr入参
:00401013 ret
void test_main(D *pd)
{
test_this_adjust(pd, &D::foo);
test_this_adjust(pd, &B::b_fun);
}
; test_this_adjust(pd, &D::foo);
:00401020 xor ecx, ecx
:00401022 push esi
:00401023 mov esi, dword ptr [esp+08] ; pd, this指针
:00401027 mov eax, 004016A0 ; D::foo vcall地址
:0040102C push ecx ; push delat = 0, ecx=0
:0040102D push eax ; push vcall_addr
:0040102E push esi ; push this
:0040102F call 00401000 ; call test_this_adjust
; test_this_adjust(pd, &B::b_fun);
:00401034 mov ecx, 00000004 ;和上面的调用不同了
:00401039 mov eax, 00401050 ; B::b_fun地址
:0040103E push ecx ; push delta = 4, exc=4
:0040103F push eax ; push vcall_addr, B::b_fun地址
:00401040 push esi ; push this
:00401041 call 00401000 ; call test_this_adjust
:00401046 add esp, 00000018
:00401049 pop esi
:0040104A ret
注意这里和上面一个例子的区别:
在调用test_this_adjust(pd, &D::foo)的时候,实际上传入了3个参数,调用相当于
test_this_adjust(pd, vcall_address_of_foo, delta(=0));
调用test_this_adjust(pd, &B::b_fun)的时候,也是3个参数
test_this_adjust(pd, vcall_address_of_b_fun, delta(=4));
两个调用有个明显的不同,就是delta的值。这个delta,为我们后来调整this指针提供了帮助。
再看看test_this_adjust函数的汇编代码,和上一个例子的不同,也就是多了一句代码:
:0040100D add ecx, eax ; this = ecx= this+delta
这就是对this指针作必要的调整。
4.1.4 结论
Microsoft根据情况选用下面的结构表示成员函数指针,使用Thunk技术(vcall_addr)实现虚拟函数/非虚拟函数的自适应,在必要的时候进行this指针调整(使用delta)。
struct pmf_type1{
void* vcall_addr;
};
struct pmf_type2{
void* vcall_addr;
int delta; //调整this指针用
};
4.2 GCC的实现
GCC对于成员函数指针的实现和Microsoft的方式有很大的不同。
4.2.1 内部表示
GCC对于成员函数指针统一使用类似下面的结构进行表示:
struct
{
void* __pfn; //函数地址,或者是虚拟函数的index
long __delta; // offset, 用来进行this指针调整
};
4.2.2 实现机制
先来看看GCC是如何区分普通成员函数和虚拟成员函数的。
不管是普通成员函数,还是虚拟成员函数,信息都记录在__pfn里面。这里有个小小的技巧。我们知道一般来说因为对齐的关系,函数地址都至少是4字节对齐的。这就意味这一个函数的地址,最低位两个bit总是0。(就算没有这个对齐限制,编译器也可以这样实现。) GCC充分利用了这两个bit。如果是普通的函数,__pfn记录该函数的真实地址,最低位两个bit就是全0,如果是虚拟成员函数,最后两个bit不是0,剩下的30bit就是虚拟成员函数在函数表中的索引值。
使用的时候,GCC先取出最低位两个bit看看是不是0,若是0就拿这个地址直接进行函数调用。若不是0,就取出前面30位包含的虚拟函数索引,通过计算得到真正的函数地址,再进行函数调用。
GCC和Microsoft对这个问题最大的不同就是GCC总是动态计算出函数地址,而且每次调用都要判断是否为虚拟函数,开销自然要比Microsoft的实现要大一些。这也差不多可以算成一种时间换空间的做法。
在this指针调整方面,GCC和Mircrosoft的做法是一样的。不过GCC在任何情况下都会带上__delta这个变量,如果不需要调整,__delta=0。
这样GCC的实现比起Microsoft来说要稍简单一些。在所有场合其实现方式都是一样的。而且这样的实现也带来多一些灵活性。这一点下面“陷阱”一节再进行说明。
[解决办法]
#include <stdio.h>#include <stdlib.h>class cmd {public: cmd() { table[0] = &cmd::func; } void func() { printf("ca\n"); } typedef void (cmd::*pfunc)(); pfunc table[10];};int main(int argc, char* argv[]) { cmd c; (c.*c.table[0])(); return 0;}