来看看怎么在C++中实现回调吧。
Method1:使用全局函数作为回调
在C语言中的回调很方便。当然,我们可以在C++中使用类似于C方式的回调函数,也就是将全局函数定义为回调函数,然后再供我们调用。
typedef void(*pCalledFun)(int *);
void GetCallBack(pCalledFun parafun)
{
/*do something*/
}
如果我们想使用GetCallBack函数,那么就要实现一个pCalledFun类型的回调函数:
void funCallback(int *iNum)
{
/* do something */
}
然后,就可以直接把funCallback当作一个变量传递给GetCallBack,
GetCallBack(funCallback);
编译器可能会有几种调用规范。比如在Visual C++中,可以在函数类型前加_cdecl,_stdcall来表示其调用规范(默认为_cdecl)。调用规范影响编译器产生的给定函数名,参数传递的顺序(从右到左或从左到右),堆栈清理责任(调用者或者被调用者)以及参数传递机制(堆栈,CPU寄存器等)。看看下面的例子:
- #include <iostream>
- using namespace std;
- typedef void (__stdcall *pFun)(void);
- typedef void (__cdecl *pFunc)(void);
- void __stdcall TextPrint(void)
- {
- cout << "Call Back Like Pascal" << endl;
- }
- void __cdecl TextPrintc(void)
- {
- cout << "Call Back Like C" << endl;
- }
- void ForText(pFun pFun1, pFunc pFun2)
- {
- pFun1();
- pFun2();
- }
- void main(void)
- {
- //pFun pP = TextPrint;
- //pFunc pPC = TextPrintc;
- //pP();
- //pPC();
- ForText(TextPrint, TextPrintc);
- }
Method2:使用类的静态函数作为回调
既然使用了C++,就不能总是生活在C的阴影中,我们要使用类,类,类!!!
下面我们来使用类的静态函数作为回调,为啥先说静态函数,因为静态函数跟全局函数很类似,函数调用时不会使用this指针,我们可以像用全局函数一样使用静态函数。如下:
- #include <iostream>
- using namespace std;
- typedef void (*pFun)(void);
- class CCallBack
- {
- public:
- static void TextPrint(void)
- {
- cout << "Static Callback Function of a Class" << endl;
- }
- };
- void ForText(pFun pFun1)
- {
- pFun1();
- }
- void main(void)
- {
- ForText(CCallBack::TextPrint);
- }
当然,我们可以把typedef封装到类中,加大内聚。
- #include <iostream>
- using namespace std;
- class CCallBack
- {
- public:
- typedef void (*pFun)(void);
- static void TextPrint(void)
- {
- cout << "Static Callback Function of a Class with funtype" << endl;
- }
- };
- void ForText(CCallBack::pFun pFun1)
- {
- pFun1();
- }
- void main(void)
- {
- ForText(CCallBack::TextPrint);
- }
Method3:使用仿函数作为回调
上面两种方法用来用去感觉还是在用C的方式,既然是C++,要面向对象,要有对象!那么就来看看仿函数吧。所谓仿函数,就是使一个类的使用看上去象一个函数,实质就是在类中重载操作符operator(),这个类就有了类似函数的行为,就是一个仿函数类了。这样的好处就是可以用面向对象的考虑方式来设计、维护和管理你的代码。多的不说,见例子:
- #include <iostream>
- using namespace std;
- typedef void(*Fun)(void);
- inline void TextFun(void)
- {
- cout << "Callback Function" << endl;
- }
- class TextFunor
- {
- public:
- void operator()(void) const
- {
- cout << "Callback Functionor" << endl;
- }
- };
- void ForText(Fun pFun, TextFunor cFun)
- {
- pFun();
- cFun();
- }
- void main(void)
- {
- TextFunor cFunor;
- ForText(TextFun, cFunor);
- }
援引一点关于仿函数的介绍吧:
仿函数(functor)的优点
我的建议是,如果可以用仿函数实现,那么你应该用仿函数,而不要用回调。
原因在于:
仿函数可以不带痕迹地传递上下文参数。 而回调技术通常 使用一个额外的 void*参数传递。这也是多数人认为回 调技术丑陋的原因。
更好的性能。 仿函数技术可以获得更好的性能, 这点直观来讲比较难以理解。 你可能说,回调函数申明为 inline了,怎么会性能比仿函数差?我们这里来分析下。我们假设某个函数 func(例如上面的 std::sort)调用中传递了一个回调函数(如上面的 compare),那么可以分为两种情况:
func 是内联函数,并且比较简单,func 调用最终被展开了,那么其中对回调函数的调用也成为一普通函数调用 (而不是通过函数指针的间接调用),并且如果这个回调函数如果简单,那么也可能同时被展开。在这种情形 下,回调函数与仿函数性能相同。
func 是非内联函数,或者比较复杂而无法展开(例如上面的 std::sort,我们知道它是快速排序,函数因为存在递归而无法展开)。此时回调函数作为一个函数指针传 入,其代码亦无法展开。而仿函数则不同。虽然 func 本 身复杂不能展开,但是 func 函数中对仿函数的调用是编 译器编译期间就可以确定并进行 inline 展开的。因此在 这种情形下,仿函数比之于回调函数,有着更好的性能。 并且,这种性能优势有时是一种无可比拟的优势(对于 std::sort 就是如此,因为元素比较的次数非常巨大,是 否可以进行内联展开导致了一种雪崩效应)。
仿函数(functor)不能做的
话又说回来了,仿函数并不能完全取代回调函数所有的应用场合。例如,我在 std::AutoFreeAlloc 中使用了回调函数,而不是仿函数, 这是因为 AutoFreeAlloc 要容纳异质 的析构函数,而不是只支持某一种类的析构。这和模板(template)不能处理在同一个容器中 支持异质类型,是一个道理。
Method4:使用类的非静态函数作为回调(采用模板的方法)
现在才开始说使用类的非静态方法作为回调是这样的,C++本身并不提供将类的方法作为回调函数的方案,而C++类的非静态方法包含一个默认的参数:this指针,这就要求回调时不仅需要函数指针,还需要一个指针指向某个实例体。解决方法有几种,使用模板和编译时的实例化及特化就是其中之一,看例子:
- #include <iostream>
- using namespace std;
- template < class Class, typename ReturnType, typename Parameter >
- class SingularCallBack
- {
- public:
- typedef ReturnType (Class::*Method)(Parameter);
- SingularCallBack(Class* _class_instance, Method _method)
- {
- class_instance = _class_instance;
- method = _method;
- };
- ReturnType operator()(Parameter parameter)
- {
- return (class_instance->*method)(parameter);
- };
- ReturnType execute(Parameter parameter)
- {
- return operator()(parameter);
- };
- private:
- Class* class_instance;
- Method method;
- };
- class CCallBack
- {
- public:
- int TextPrint(int iNum)
- {
- cout << "Class CallBack Function" << endl;
- return 0;
- };
- };
- template < class Class, typename ReturnType, typename Parameter >
- void funTest(SingularCallBack<Class, ReturnType, Parameter> tCallBack)
- {
- tCallBack(1);
- }
- void main(void)
- {
- CCallBack callback;
- SingularCallBack<CCallBack, int, int> Test(&callback, callback.TextPrint);
- Test.execute(1);
- Test(1);
- funTest(Test);
- }
Method5:使用类的非静态函数作为回调(采用thunk的方法1)
所谓thunk,就是替换,改变系统本来的调用意图,也有的说是用机器码代替系统调用。
替换原来意图,转调我们需要的地址。 网上有段解释是这样“巧妙的将数据段的几个字节的数据设为特殊的值,然后告诉系统,这几个字节的数据是代码(即将一个函数指针指向这几个字节的第一个字节)”。
为什么不能直接使用类的非静态函数作为回调函数呢,通俗点的解释就是类的非静态函数都要默认传入一个this指针参数,这就跟我们平时的回调不同了,所以无法使用。
上面提到过,一般的回调函数都是_stdcall或者_cdecl的调用方式,但是成员函数是__thiscall的调用方式。这种调用方式的差别导致不能直接使用类的非静态成员函数作为回调函数。看看区别吧:
关键字 | 堆栈清除 | 参数传递 |
__stdcall | 被调用者 | 将参数倒序压入堆栈(自右向左) |
__thiscall | 被调用者 | 压入堆栈,this指针保存在 ECX 寄存器中 |
可见两者的不同之处就是_thiscall把this指针保存到了ECX的寄存器中,其他都是一样的。所以我们只需要在调用过程中首先把this指针保存到ECX,然后跳转到期望的成员函数地址就可以了。代码如下:
- #include <tchar.h>
- #include <wtypes.h>
- #include <iostream>
- using namespace std;
- typedef void (*FUNC)(DWORD dwThis);
- typedef int (_stdcall *FUNC1)(int a, int b);
- #pragma pack(push,1)
- //先将当前字节对齐值压入编译栈栈顶, 然后再将 n 设为当前值
- typedef struct tagTHUNK
- {
- BYTE bMovEcx; //MOVE ECX Move this point to ECX
- DWORD dwThis; // address of this pointer
- BYTE bJmp; //jmp
- DWORD dwRealProc; //proc offset Jump Offset
- void Init(DWORD proc,void* pThis)
- {
- bMovEcx = 0xB9;
- dwThis = (DWORD)pThis;
- bJmp = 0xE9;
- dwRealProc = DWORD((INT_PTR)proc - ((INT_PTR)this+sizeof(THUNK)));
- //jmp跳转的是当前指令地址的偏移,也就是成员函数地址与当前指令的地址偏移
- FlushInstructionCache(GetCurrentProcess(),this,sizeof(THUNK));
- // 因为修改了数据,所以调用FlushInstructionCache,刷新缓存
- }
- }THUNK;
- // BYTE bMovEcx; DWORD dwThis; 这两句连起来就是把this指针保存到了ECX的寄存器
- // BYTE bJmp; DWORD dwRealProc;就是跳转到成员函数的地址
- #pragma pack(pop)
- //将编译栈栈顶的字节对齐值弹出并设为当前值.
- template<typename dst_type, typename src_type>
- dst_type pointer_cast(src_type src)
- {
- return *static_cast<dst_type*>(static_cast<void*>(&src));
- }
- class Test
- {
- public:
- int m_nFirst;
- THUNK m_thunk;
- int m_nTest;
- Test() : m_nTest(3),m_nFirst(4)
- {}
- void TestThunk()
- {
- m_thunk.Init(pointer_cast<int>(&Test::Test2),this);
- FUNC1 f = (FUNC1)&m_thunk;
- f(1,2);
- cout << "Test::TestThunk()" << endl;
- }
- int Test2(int a, int b)
- {
- cout << a << " " << b << " " << m_nFirst << " " << m_nTest << " <<I am in Test2" << endl;
- return 0;
- }
- };
- int main(int argc, _TCHAR* argv[])
- {
- Test t;
- t.TestThunk();
- //system("pause");
- return 0;
- }
PS:可以看出上面的方法是将代码写入数据段,达到了强制跳转的目的,在这个过程中一定要弄清楚函数调用规则和堆栈的平衡。
在指针转化中使用了pointer_cast函数,也可以这样进行:
template<class ToType, class FromType>
void GetMemberFuncAddr_VC6(ToType& addr,FromType f)
{
union
{
FromType _f;
ToType _t;
}ut;
ut._f = f;
addr = ut._t;
}
使用的时候:
DWORD dPtr;
GetMemberFuncAddr_VC6(dPtr,Class::Function); //取成员函数地址.
FUNCTYPE pFunPtr = (FUNCTYPE) dPtr;//将函数地址转化为普通函数的指针
因为在类的方法默认的调用规则是thiscall,所以上面在进行回调的过程中采用了在ecx中传入this指针的方法,也就是this指针通过ecx寄存器进行传递。注意,在VC6中是没有__thiscall关键字的,如果使用了编译器会报错。
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Method6:使用类的非静态函数作为回调(采用thunk的方法2)
在上面的实现过程中,可以看出来主要的部分就是这里:
bMovEcx = 0xB9;
dwThis = (DWORD)pThis;
bJmp = 0xE9;
dwRealProc = DWORD((INT_PTR)proc - ((INT_PTR)this+sizeof(THUNK)));
意思就是Move Ecx pThis; 即把this指针保存到了ECX的寄存器,JMP dwRealProc;就是跳转到成员函数的地址并进行调用。
对于调用类的非静态方法时中需要传入this指针的问题,我们再找一种方法进行解决,上面的类方法使用的是默认的thiscall的调用规则,下面我们使用stdcall的方式,也就是不使用ecx传递this指针,而直接使用栈进行this指针的传递。见代码:
- #include <iostream>
- #include <windows.h>
- using namespace std;
- template<typename ToType, typename FromType>
- void GetMemberFuncAddr_VC6(ToType& addr,FromType f)
- {
- union
- {
- FromType _f;
- ToType _t;
- }ut;
- ut._f = f;
- addr = ut._t;
- }
- class Test
- {
- public:
- void __stdcall Print(int x,char c,char *s)
- {
- cout << "m_a=" << m_a << "," << x << c << s << endl;
- }
- int m_a;
- };
- void main(void)
- {
- typedef void (__stdcall *FUNCTYPE)(void *This,int x,char c,char *s);
- Test test;
- test.m_a = 111;
- DWORD ptr;
- GetMemberFuncAddr_VC6(ptr,Test::Print);
- FUNCTYPE fnPrintPtr = (FUNCTYPE)ptr;
- fnPrintPtr(&test,3,'Q',"abcde");
- }
这里我们使用stdcall的约定定义函数方法,在调用的过程中在函数的参数列表的最左端添入一个this指针参数,也就是在栈的最下面,函数返回值之前加入this指针,达到可以调用的目的。
Method7:使用类的非静态函数作为回调(采用直接调用虚函数的方法)
其实这种方法跟上面的也类似,规避this指针的方法也是采用嵌入汇编的方式,将this指针赋值到ecx中。不过就是获取函数地址的方法有点不同了,上面都是使用pointer_cast<int>(&Test::Test2)这种方法,直接转化类的方法,这次我们将类函数的写成虚函数,通过虚函数表获取函数地址,进行调用。
- #include <windows.h>
- #include <iostream>
- using namespace std;
- class Test {
- public:
- Test()
- {
- m_nFlag = 10;
- }
- virtual void f(int nNum)
- {
- cout<<"The number is "<<nNum<< " ,Flag is " <<m_nFlag << endl;
- }
- int m_nFlag;
- };
- void main(void)
- {
- typedef void(__stdcall *Fun)(int);
- Test test;
- DWORD pThis = (DWORD)&test;
- int i = 1250;
- //-----------------------------------------
- int** pVtbl = (int**)&test;
- Fun pFun = (Fun) pVtbl[0][0];
- _asm
- {
- mov ecx,pThis;
- }
- pFun(i);
- //-----------------------------------------
- //-----------------------------------------
- pFun = (Fun)*((DWORD*)*(DWORD*)(&test));
- _asm
- {
- mov ecx,pThis;
- }
- pFun(i);
- //-----------------------------------------
- }
PS:上面的代码中使用了两种方法获取函数的地址,好好琢磨一下吧~
Method8:使用类的非静态函数作为回调(采用成员函数指针的方法)
使用成员函数指针,不过这个弄的有点不像回调了,仅供参考吧:
- #include <iostream>
- using namespace std;
- class CTest{
- public:
- CTest(int nNum) : m_nNum(nNum)
- {
- }
- int m_nNum;
- void func(int x, char *p) {
- cout << m_nNum << x << p << endl;;
- };
- };
- typedef void (CTest::*Func)(int, char *);
- void CallFun(CTest* pCls, Func pFunc,int x, char* p)
- {
- (pCls->*pFunc)(x, p);
- }
- int main() {
- Func pFunc;
- pFunc = CTest::func;
- CTest test(5);
- CTest* pClass = &test;
- (pClass->*pFunc)(1, "234");
- CallFun(pClass, pFunc, 2, "abc");
- return 0;
- }
Method9:使用类的非静态函数作为回调(采用FastDelegate)
很多大牛们早就开始研究这个问题了,解决方法也有很多,FastDelegate就是一个,可以参看下面的网址:
http://www.codeproject.com/KB/cpp/FastDelegate.aspx
当然中译本也有了,搜一下“成员函数指针与高性能的C++委托”就可以了,不多说了。
Method10:使用类的非静态函数作为回调(采用Tr1::function + bind)
C++ Technical Report 1 (TR1)是ISO/IEC TR 19768, C++ Library Extensions(函式库扩充)的一般名称。TR1是一份文件,内容提出了对C++标准函式库的追加项目。这些追加项目包括了正则表达式、智能指针、哈希表、随机数生成器等。TR1自己并非标准,他是一份草稿文件。然而他所提出的项目很有可能成为下次的官方标准。这份文件的目标在于「为扩充的C++标准函式库建立更为广泛的现成实作品」。
C++ tr1是针对C++标准库的第一次扩展。即将到来的下一个版本的C++标准c++0x会包括它,以及一些语言本身的扩充。tr1包括大家期待已久的smart pointer,正则表达式以及其他一些支持范型编程的东东。草案阶段,新增的类和模板的名字空间是std::tr1。
这个没怎么研究过,先列到这里,以后再慢慢研究~~
Method11:使用类的非静态函数作为回调(采用Boost::Function + bind)
BOOST库接触的比较少,也不敢多说啥,看到网上写的供给回调的方案有下面几种吧
Boost::Function + bind
Boost::Functor + Signal/Slot
Boost::lamda
大家可以研究一下,这个也以后再细细的说吧~
PS:大家可以看看下面的这个博客,讨论了一下各种方法的速度,没有具体考察过,大家权作参考吧~
http://www.cppblog.com/oldworm/archive/2011/01/30/139610.html
方法太多了,用的场合也多种多样,还要细细的研究啊~
回调,To be, or not to be...