什么是回调?通常发生在需要两个角色即调用者与实现者的情形上,即我们希望当产生某个事件时,调用实现者定义的某个函数。当然这个概念很大,不是说操作系统的信号量,条件变量什么的,是在语言级别实现,如一个Framework提供商,规定了整个程序的框架,可能产生某事件时它希望调用某个行为,而这个行为的具体定义是由framework客户来完成。

我们从简单的做起,通过一个个为什么最终来获得一个比较好的回调实现。

C语言中用全局函数实现回调最简单了:

  1.  
  2. void callback(int a)
  3. {
  4.     cout<<"callback called with para="<<a<<endl;
  5. }
  6.  
  7. typedef void (*pfunc)(int);
  8. void caller(pfunc p)
  9. {
  10.     (*p)(1);
  11. }
  12.  
  13. int main(int argc, char* argv[])
  14. { 
  15.     caller(&callback);
  16. }

相信不用多解释了吧, 可是到了面向对象的世界里,就不是那么简单了,如果回调函数是成员函数怎么办?

非静态成员函数作回调函数

当然如果是静态成员函数就好办跟全局函数是类似,到此为止世界还没有变乱,如在VC编程中用AfxBeginThread开启一个线程,就经常将参数AFX_THREADPROC pfnThreadProc定义为一个全局函数或静态成员函数,可是这两个都不方便访问类的非静态成员,之所以郑重其事地写这篇文章,就是以前静态回调用起来非常不爽。

回调函数是非静态成员函数呢?我们可不能简单地设为这样:

  1.  
  2. class CCallback
  3. {
  4. public:
  5.     void Func(int a)
  6.     {
  7.         cout<<"member function callback called with para="<<a<<endl;
  8.     }
  9. };
  10. typedef void (CCallback::*pMemberFunc)(int);
  11. void Caller(pMemberFunc p)
  12. {
  13.     (*p)(1);
  14. }

这样编译就不会通过的,因为非静态的成员函数必须通过对象来访问,好,我们稍稍改进一下:

  1.  
  2. class CCallback
  3. {
  4. public:
  5.     void Func(int a)
  6.     {
  7.         cout<<"member function callback called with para="<<a<<endl;
  8.     }
  9. };
  10. typedef void (CCallback::*pMemberFunc)(int);
  11. void Caller(CCallback* pObj,pMemberFunc p)
  12. {
  13.     (pObj->*p)(1);
  14. }
  15.  
  16. int main(int argc, char* argv[])
  17. { 
  18.     CCallback obj;
  19.     Caller(&obj,&CCallback::Func);
  20. }

即给Caller多传个对象进去,好吧,貌似问题解决了,可是,调用者(如库的提供商)只知道回调函数接口长这样而已,事先全然不知客户的类是如何定义,终于模板登上场了:

  1. template<typename T>
  2. void Caller(T* pObj,void (T::*p)(int))
  3. {
  4.     (pObj->*p)(1);
  5. }

其他不变的,把调用者这里换成模板就OK了,当然这个Caller也可以是成员函数,现在用这个方法写个小应用是没什么问题了,但是限制多多,如调用者一次只调用了一个实现,但现实情况往往是产生某个事件时,应该依次调用多个行为,即把挂在这个事件上的所有回调函数通通临幸一遍,还有回调是如此的重要,以至于C#不用库在语言本身层面就实现了它,我们也不可以到此草草了事,而是按照组件化的思维提供一套完善的回调机制,所谓完善,如上个例子中Caller只能接收一个参数为int,返回值为void的成员函数指针,等等,必须是这样的接口吗,想想参数为double行不行,如void (T::*p)(double)这样的函数传给它可以吗,int不是可自动转换为double吗,那这个函数指针也能自动转换吗,就像C#中的协变与逆变一样,不行,C++不允许,当然我们可以强制转换,不过要在十分清楚类型的情况下才能这么做,否则因为不是类型安全的很容易引起程序错误甚至崩溃。所以要支持各种参数,多个参数,还得模板,嗯嗯,努力尚未成功,同志还需革命!

多态回调

甭管什么名词,总之我们的目的是:产生某个事件时,调用某个待客户实现的行为,调用者什么时候调用确定了,关键是客户按照规定接口实现这个行为,这听起来有点像多态了,是的,有时候被调用者与调用者是继承关系,这就不需要其它理论了,就多态呗,不过多态不一定非得用虚函数来实现,就像MFC一样,考虑到每个类背负一个庞大的虚函数表会带来很大的性能损失,换做用几个结构体和强大的宏而实现消息映射。在wincore.cpp中,CWnd::OnWndMsg源码里,当来了消息,在事先建立的链表中从派生类依次向上查找第一个实现了这个消息的类的AFX_MSGMAP结构体,再取得它的AFX_MSGMAP_ENTRY成员,即真正的消息入口地址,

  1.  
  2. struct AFX_MSGMAP_ENTRY
  3. {
  4.     UINT nMessage;   // windows message
  5.     UINT nCode;      // control code or WM_NOTIFY code
  6.     UINT nID;        // control ID (or 0 for windows messages)
  7.     UINT nLastID;    // used for entries specifying a range of control id's
  8.     UINT nSig;       // signature type (action) or pointer to message #
  9.     AFX_PMSG pfn;    // routine to call (or special value)
  10. };

就类似于写一个普通的链表结构:struct list_node{list_node* next; int data},只不过这里的链表的next不能再随便指,要指向基类的节点,根据next指针找到对应的节点后取出数据data成员即可,在这里,data就是AFX_MSGMAP_ENTRY,如上图,AFX_MSGMAP_ENTRY里定义了消息标号即各种附加参数,还有最关键的成员pfn,代表了事先派生类通过宏填充好的回调成员函数地址。但是pfn的类型即AFX_PMSG定义为typedef void (AFX_MSG_CALL CCmdTarget::*AFX_PMSG)(void); 只能代表一种类型,而客户的派生类的为响应消息的回调函数的类型有很多种,在框架中如何保证以正确的形式调用呢?原来客户在填充消息标号和函数地址时,也顺便填充好了函数类型交给nSig成员保存,根据nSig,如前文所说,将pfn强制转换到相应的类型就OK了,不过这成员函数指针转换来转换去,代码非常难看啊可读性不强,于是使用union进行类型转换:

  1.  
  2. //afximpl.h
  3. union MessageMapFunctions
  4. {
  5.     AFX_PMSG pfn;   // generic member function pointer
  6.  
  7.     // specific type safe variants for WM_COMMAND and WM_NOTIFY messages
  8.     void (AFX_MSG_CALL CCmdTarget::*pfn_COMMAND)();
  9.     BOOL (AFX_MSG_CALL CCmdTarget::*pfn_bCOMMAND)();
  10.     void (AFX_MSG_CALL CCmdTarget::*pfn_COMMAND_RANGE)(UINT);
  11.     BOOL (AFX_MSG_CALL CCmdTarget::*pfn_COMMAND_EX)(UINT);
  12. ...
  13. }
  14.  
  15. //wincore.cpp  CWnd::OnWndMsg
  16. union MessageMapFunctions mmf;
  17. mmf.pfn = lpEntry->pfn;
  18. nSig = lpEntry->nSig;
  19. switch (nSig)
  20.     {
  21.     default:
  22.         ASSERT(FALSE);
  23.         break;
  24.  
  25.     case AfxSig_bD:
  26.         lResult = (this->*mmf.pfn_bD)(CDC::FromHandle((HDC)wParam));
  27.         break;
  28.  
  29.     case AfxSig_bb:     // AfxSig_bb, AfxSig_bw, AfxSig_bh
  30.         lResult = (this->*mmf.pfn_bb)((BOOL)wParam);
  31.         break;
  32.  
  33.     case AfxSig_bWww:   // really AfxSig_bWiw
  34.         lResult = (this->*mmf.pfn_bWww)(CWnd::FromHandle((HWND)wParam),
  35.             (short)LOWORD(lParam), HIWORD(lParam));
  36.         break;
  37. ...
  38. }

当然这里只是一个小插曲而已,它只是MFC为满足于自己应用设计这么一套机制,派生类的回调函数类型是有限的,再则要求与框架类是继承关系,如果没有继承关系怎么办,例如当产生串口或者网口收到数据的事件时,需要更新UI界面,UI界面与串口类可是没有丝毫继承关系的,呃...铁人王进喜说:有条件要上,没条件创造条件也要上,我们大不了专门定义一个回调抽象类,让UI界面继承自它,实现类里的回调函数,然后串口类通过抽象类型对象指针就可以多态地调用到UI的真正回调实现。COM/ATL的回调,Java的回调就是这么干。不过在C++中,情形有些不一样,这样实现很勉强,它需要多重继承,仍然不能直接实现同时调用多个行为,耦合性高,每个回调都需要单独定义一个类(只要接口不一样),效率也不够高,我们想直接调用到绑定好的回调,基于这些缺点,还得寻找更好的方法。

信号与槽(Signal/Slots)

说了这么多,终于来到正题了,在C++中,信号与槽才是回调的完美解决方案,其实本质上是一个观察者模式,包括其它的叫法:delegate,notifier/receiver,observer,C#中的delegate也是一个观察者的实现。Qt中提供了信号与槽的整套机制,任何对象的槽可以绑定到另一个对象的信号上,一个信号可以拥有多个槽,经典的图例如下:

可是qt中的实现用了signal slot关键字,不是C++标准的啊,其它编译器不能随便编译(好像先经过qmake生成标准的代码就可以了),直接上源码不妥得搞清楚为什么,一切从最简单的入手,我们先来用标准C++实现一个简易的signal/slots,如何实现呢,说白了,就是想方设法把回调函数信息保存起来,必要时利用它就OK了,回调函数信息就两个,类对象指针与成员函数地址,我们将这对信息存储到名叫slot的类中,而在signal类中,维护多个slot即可,仍然用带一个int参数,返回值为void的函数接口:

  1.  
  2. #include <vector>
  3. #include <iostream>
  4. using namespace std;
  5.  
  6. template<typename T, typename T1>
  7. class slot
  8. {
  9. public:
  10.     slot(T* pObj,void (T::*pMemberFunc)(T1))
  11.     {
  12.         m_pObj=pObj;
  13.         m_pMemberFunc=pMemberFunc;
  14.     }
  15.     void Execute(T1 para)
  16.     {
  17.         (m_pObj->*m_pMemberFunc)(para);
  18.     }
  19. private:
  20.     T* m_pObj;
  21.     void (T::*m_pMemberFunc)(T1);
  22. };
  23.  
  24. template<typename T, typename T1>
  25. class signal
  26. {
  27. public:
  28.     void bind(T* pObj,void (T::*pMemberFunc)(T1 para))
  29.     {
  30.         m_slots.push_back(new slot<T,T1>(pObj,pMemberFunc));
  31.     }
  32.     ~signal()
  33.     {
  34.         vector<slot<T,T1>* >::iterator ite=m_slots.begin();
  35.         for (;ite!=m_slots.end();ite++)
  36.         {
  37.             delete *ite;
  38.         }
  39.     }
  40.     void operator()(T1 para)
  41.     {
  42.         vector<slot<T,T1>* >::iterator ite=m_slots.begin();
  43.         for (;ite!=m_slots.end();ite++)
  44.         {
  45.             (*ite)->Execute(para);
  46.         }
  47.     }
  48.     
  49. private:
  50.     vector<slot<T,T1>* > m_slots;
  51. };
  52.  
  53. class receiver
  54. {
  55. public:
  56.     void callback1(int a)
  57.     {
  58.         cout<<"receiver1: "<<a<<endl;
  59.     }
  60.     void callback2(int a)
  61.     {
  62.         cout<<"receiver2: "<<a<<endl;
  63.     }
  64. };
  65.  
  66. class sender
  67. {
  68. public:
  69.     sender(): m_value(0)  {}
  70.     int get_value()
  71.     {
  72.         return m_value;
  73.     }
  74.     void set_value(int new_value)
  75.     {
  76.         if (new_value!=m_value)
  77.         {
  78.             m_value=new_value;
  79.             m_sig(new_value);
  80.         }
  81.     }
  82.     signal<receiver,int> m_sig;
  83. private:
  84.     int m_value;
  85. };
  86.  
  87. int main(int argc,char** arg)
  88. {
  89.     receiver r;
  90.     sender s;
  91.     s.m_sig.bind(&r,&receiver::callback1);
  92.     s.m_sig.bind(&r,&receiver::callback2);
  93.     s.set_value(1);
  94.     return 0;
  95. }

程序在VC6下顺利通过,这个版本相比前面所说的继承手法耦合性低了,被调用者receiver与规定函数接口的slot类没有任何关系,但仔细以观察这个程序在概念上是有问题的,signal类有两个模板参数,一个是类的类型,一个是函数参数类型,如果把这个signal/slots组件提供出去,使用者如上面的sender类不免会有个疑虑:在实例化signal类型时,必须提供这两个模板参数,可是调用方事先哪就一定知道接收方(receiver)的类型呢,而且从概念上讲事件发送方与接收方只需遵循一个共同的函数接口就可以了,与类没什么关系,上个程序要求在实例化时就得填充receiver的类型,也就决定了它与receiver只能一对一,而不能一对多,于是作此改进:将signal的参数T去掉,将T类型的推导延迟到绑定(bind)时,signal没有参数T,signal的成员slot也就不能有,那slot的成员也就不能有,可是,参数T总得找个地方落脚啊,怎么办?有个窍门:让slot包含slotbase成员,slotbase没有参数T的,但slotbase只定义接口,真正的实现放到slotimpl中,slotimpl就可以挂上参数T了,boost中any、shared_ptr就是用此手法,改进后全部代码如下:

  1.  
  2. #include <vector>
  3. #include <iostream>
  4. using namespace std;
  5.  
  6. template<typename T1>
  7. class slotbase
  8. {
  9. public:
  10.     virtual void Execute(T1 para)=0;
  11. };
  12.  
  13. template<typename T,typename T1>
  14. class slotimpl : public slotbase<T1>
  15. {
  16. public:
  17.     slotimpl(T* pObj,void (T::*pMemberFunc)(T1))
  18.     {
  19.         m_pObj=pObj;
  20.         m_pMemberFunc=pMemberFunc;
  21.     }
  22.     virtual void Execute(T1 para)
  23.     {
  24.         (m_pObj->*m_pMemberFunc)(para);
  25.     }
  26. private:
  27.     T* m_pObj;
  28.     void (T::*m_pMemberFunc)(T1);
  29. };
  30.  
  31. template<typename T1>
  32. class slot 
  33. {
  34. public:
  35.     template<typename T>
  36.         slot(T* pObj,void (T::*pMemberFunc)(T1)) 
  37.     {
  38.         m_pSlotbase=new slotimpl<T,T1>(pObj,pMemberFunc);
  39.     }
  40.     ~slot()
  41.     {
  42.         delete m_pSlotbase;
  43.     }
  44.     void Execute(T1 para)
  45.     {
  46.         m_pSlotbase->Execute(para);
  47.     }
  48. private:
  49.     slotbase<T1>* m_pSlotbase;
  50. };
  51.  
  52. template<typename T1>
  53. class signal
  54. {
  55. public:
  56.     template<typename T>
  57.     void bind(T* pObj,void (T::*pMemberFunc)(T1 para))
  58.     {
  59.         m_slots.push_back(new slot<T1>(pObj,pMemberFunc));
  60.     }
  61.     ~signal()
  62.     {
  63.         vector<slot<T1>* >::iterator ite=m_slots.begin();
  64.         for (;ite!=m_slots.end();ite++)
  65.         {
  66.             delete *ite;
  67.         }
  68.     }
  69.     void operator()(T1 para)
  70.     {
  71.         vector<slot<T1>* >::iterator ite=m_slots.begin();
  72.         for (;ite!=m_slots.end();ite++)
  73.         {
  74.             (*ite)->Execute(para);
  75.         }
  76.     }
  77.     
  78. private:
  79.     vector<slot<T1>* > m_slots;
  80. };
  81.  
  82. #define CONNECT(sender,signal,receiver,slot)  sender.signal.bind(receiver,slot)
  83.  
  84. class receiver
  85. {
  86. public:
  87.     void callback1(int a)
  88.     {
  89.         cout<<"receiver1: "<<a<<endl;
  90.     }
  91. };
  92. class receiver2
  93. {
  94. public:
  95.     void callback2(int a)
  96.     {
  97.         cout<<"receiver2: "<<a<<endl;
  98.     }
  99. };
  100.  
  101. class sender
  102. {
  103. public:
  104.     sender(): m_value(0)  {}
  105.     int get_value()
  106.     {
  107.         return m_value;
  108.     }
  109.     void set_value(int new_value)
  110.     {
  111.         if (new_value!=m_value)
  112.         {
  113.             m_value=new_value;
  114.             m_valueChanged(new_value);
  115.         }
  116.     }
  117.     signal<int> m_valueChanged;
  118. private:
  119.     int m_value;
  120.     
  121. };
  122.  
  123. int main(int argc,char** arg)
  124. {
  125.     receiver r;
  126.     receiver2 r2;
  127.     sender s;
  128.     CONNECT(s,m_valueChanged,&r,&receiver::callback1);
  129.     CONNECT(s,m_valueChanged,&r2,&receiver2::callback2);
  130.     s.set_value(1);
  131.     return 0;
  132. }

这个版本就比较像样了,一个signal可与多个slots连接,增加了类似QT的connect,用宏实现#define CONNECT(sender,signal,receiver,slot) sender.signal.bind(receiver,slot),这样使用者就非常方便,而且现在已完全解耦,sender只管定义自己的signal,在恰当时机用仿函数形式调用即可,而receiver只管实现callback,互不影响,可独立工作,如果需要再通过CONNECT将它们连接起来即可,已经很组件化了,可是离真正的工程应用尚有一段距离,如它不能接收全局函数或静态成员函数或仿函数为回调函数,不能带两个或更多的函数参数,最后一步了。

c++ 回调的实现的更多相关文章

  1. JAVA回调机制(CallBack)详解

    序言 最近学习java,接触到了回调机制(CallBack).初识时感觉比较混乱,而且在网上搜索到的相关的讲解,要么一言带过,要么说的比较单纯的像是给CallBack做了一个定义.当然了,我在理解了回 ...

  2. 小兔JS教程(三)-- 彻底攻略JS回调函数

    这一讲来谈谈回调函数. 其实一句话就能概括这个东西: 回调函数就是把一个函数当做参数,传入另一个函数中.传进去的目的仅仅是为了在某个时刻去执行它. 如果不执行,那么你传一个函数进去干嘛呢? 就比如说对 ...

  3. iOS 键盘添加完成按钮,delegate和block回调

    这个是一个比较初级一点的文章,新人可以看看.当然实现这个需求的时候自己也有一点收获,记下来吧. 前两天产品要求在工程的所有数字键盘弹出时,上面带一个小帽子,上面安装一个“完成”按钮,这个完成按钮也没有 ...

  4. JAVA回调机制解析

    一.回调机制概述     回调机制在JAVA代码中一直遇到,但之前不懂其原理,几乎都是绕着走.俗话说做不愿意做的事情叫做突破,故诞生了该文章,算是新年的新气象,新突破!     回调机制是什么?其实回 ...

  5. 嵌入式&iOS:回调函数(C)与block(OC)传 参/函数 对比

    C的回调函数: callBack.h 1).声明一个doSomeThingCount函数,参数为一个(无返回值,1个int参数的)函数. void DSTCount(void(*CallBack)(i ...

  6. 嵌入式&iOS:回调函数(C)与block(OC)回调对比

    学了OC的block,再写C的回调函数有点别扭,对比下区别,回忆记录下. C的回调函数: callBack.h 1).定义一个回调函数的参数数量.类型. typedef void (*CallBack ...

  7. Java回调方法详解

    回调在维基百科中定义为: 在计算机程序设计中,回调函数,是指通过函数参数传递到其他代码的,某一块可执行代码的引用. 其目的是允许底层代码调用在高层定义的子程序. 举个例子可能更明白一些:以Androi ...

  8. Android开发之自定义组件和接口回调

    说到自定义控件不得不提的就是接口回调,在Android开发中接口回调用的还是蛮多的.在这篇博客开始的时候呢,我想聊一下iOS的自定义控件.在iOS中自定义控件的思路是继承自UIView, 在UIVie ...

  9. jQuery2.x源码解析(回调篇)

    jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 通过艾伦的博客,我们能看出,jQuery的pro ...

  10. 给jquery-validation插件添加控件的验证回调方法

    jquery-validation.js在前端验证中使用起来非常方便,提供的功能基本上能满足大部分验证需求,例如:1.内置了很多常用的验证方法:2.可以自定义错误显示信息:3.可以自定义错误显示位置: ...

随机推荐

  1. Code Project精彩系列(转)

    Code Project精彩系列(转)   Code Project精彩系列(转)   Applications Crafting a C# forms Editor From scratch htt ...

  2. Tinkoff Challenge - Elimination Round C. Mice problem(模拟)

    传送门 题意 给出一个矩形的左下角和右上角的坐标,给出n个点的初始坐标和运动速度和方向,询问是否存在一个时间使得所有点都在矩形内,有则输出最短时间,否则输出-1 分析 对于每个点如果运动过程中都不在矩 ...

  3. 在ios Xcode10下小白都能解决library not found for -libstdc++.6.0.9

    写在前面 library not found for -libstdc++.6.0.9,今天做项目的时候碰到这个问题,解决的过程中遇到了目录路径不对的问题(不在通常的/Applications/Xco ...

  4. react-native页面之间的相互传值

    react-native页面之间的相互传值 之前在自己学习react-native的时候,在页面跳转和传值问题上花了一段时间去网上搜索和查找资料,自己总结了两个方法.可以参考 https://blog ...

  5. [ZPG TEST 110] 多边形个数【DP】

    1. 多边形个数 (polygons.pas/c/cpp) [问题描述] 给定N线段,编号1到n.并给出这些线段的长度,用这些线段组成一个K边形,并且每个线段做多使用一次.若使用了一条不同编号的线段, ...

  6. Oracle代码 规则 创建表 表空间 用户等

    -----创建表空间----------create tablespace bdccslogging datafile 'D:\oracle\product\10.2.0\oradata\bdccs\ ...

  7. 设置UITableViewCell 选中时的背景颜色

    自定义Cell如图 一个View上面放了四个Label 分别连线到.m文件中 @property (weak, nonatomic) IBOutlet UILabel *nameLabel; @pro ...

  8. scala学习笔记4:函数和闭包

    以下主要记录的是看完scala in programming这本书functions and closures(第八章)后的要点总结. 1,函数可以存在的地方:函数方法,嵌套函数. 2,关于funct ...

  9. webapp开发学习--Ionic+Cordova 环境搭建

    我们看 Ionic 能给我们提供什么? 一个样式库,你可以使用它来装饰你的HTML网页 ,看起来 想 移动程序的界面,什么header .content.footer.grid.list.这貌似没什么 ...

  10. AJPFX关于abstract的总结

    抽象类: abstract抽象:不具体,看不明白.抽象类表象体现.在不断抽取过程中,将共性内容中的方法声明抽取,但是方法不一样,没有抽取,这时抽取到的方法,并不具体,需要被指定关键字abstract所 ...