c++ 回調的實現

什麼是回調?一般發生在須要兩個角色即調用者與實現者的情形上,即咱們但願當產生某個事件時,調用實現者定義的某個函數。固然這個概念很大,不是說操做系統的信號量,條件變量什麼的,是在語言級別實現,如一個Framework提供商,規定了整個程序的框架,可能產生某事件時它但願調用某個行爲,而這個行爲的具體定義是由framework客戶來完成。html

     咱們從簡單的作起,經過一個個爲何最終來得到一個比較好的回調實現。node

C語言中用全局函數實現回調最簡單了:ios

複製代碼
複製代碼
void callback( int a)
{
    cout<< " callback called with para= "<<a<<endl;
}

typedef  void (*pfunc)( int);
void caller(pfunc p)
{
    (*p)( 1);
}

int main( int argc,  char* argv[])

    caller(&callback);
}
複製代碼
複製代碼

      相信不用多解釋了吧, 但是到了面向對象的世界裏,就不是那麼簡單了,若是回調函數是成員函數怎麼辦?編程

非靜態成員函數做回調函數windows

      固然若是是靜態成員函數就好辦跟全局函數是相似,到此爲止世界尚未變亂,如在VC編程中用AfxBeginThread開啓一個線程,就常常將參數AFX_THREADPROC pfnThreadProc定義爲一個全局函數或靜態成員函數,但是這兩個都不方便訪問類的非靜態成員,之因此鄭重其事地寫這篇文章,就是之前靜態回調用起來很是不爽。安全

      回調函數是非靜態成員函數呢?咱們可不能簡單地設爲這樣:框架

複製代碼
複製代碼
class CCallback
{
public:
     void Func( int a)
    {
        cout<< " member function callback called with para= "<<a<<endl;
    }
};
typedef  void (CCallback::*pMemberFunc)( int);
void Caller(pMemberFunc p)
{
    (*p)( 1);
}
複製代碼
複製代碼

     這樣編譯就不會經過的,由於非靜態的成員函數必須經過對象來訪問,好,咱們稍稍改進一下:函數

複製代碼
複製代碼
class CCallback
{
public:
     void Func( int a)
    {
        cout<< " member function callback called with para= "<<a<<endl;
    }
};
typedef  void (CCallback::*pMemberFunc)( int);
void Caller(CCallback* pObj,pMemberFunc p)
{
    (pObj->*p)( 1);
}

int main( int argc,  char* argv[])

    CCallback obj;
    Caller(&obj,&CCallback::Func);
}
複製代碼
複製代碼

      即給Caller多傳個對象進去,好吧,貌似問題解決了,但是,調用者(如庫的提供商)只知道回調函數接口長這樣而已,事先全然不知客戶的類是如何定義,終於模板登上場了:組件化

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

     其餘不變的,把調用者這裏換成模板就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成員,即真正的消息入口地址,

複製代碼
複製代碼
struct AFX_MSGMAP_ENTRY
{
    UINT nMessage;    //  windows message
    UINT nCode;       //  control code or WM_NOTIFY code
    UINT nID;         //  control ID (or 0 for windows messages)
    UINT nLastID;     //  used for entries specifying a range of control id's
    UINT nSig;        //  signature type (action) or pointer to message #
    AFX_PMSG pfn;     //  routine to call (or special value)
};
複製代碼
複製代碼

     就相似於寫一個普通的鏈表結構: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進行類型轉換:

複製代碼
複製代碼
// afximpl.h
union MessageMapFunctions
{
    AFX_PMSG pfn;    //  generic member function pointer

    
//  specific type safe variants for WM_COMMAND and WM_NOTIFY messages
     void (AFX_MSG_CALL CCmdTarget::*pfn_COMMAND)();
    BOOL (AFX_MSG_CALL CCmdTarget::*pfn_bCOMMAND)();
     void (AFX_MSG_CALL CCmdTarget::*pfn_COMMAND_RANGE)(UINT);
    BOOL (AFX_MSG_CALL CCmdTarget::*pfn_COMMAND_EX)(UINT);
...
}

//wincore.cpp   CWnd::OnWndMsg
union MessageMapFunctions mmf;
mmf.pfn = lpEntry->pfn;
nSig = lpEntry->nSig;
switch (nSig)
    {
     default:
        ASSERT(FALSE);
         break;

     case AfxSig_bD:
        lResult = ( this->*mmf.pfn_bD)(CDC::FromHandle((HDC)wParam));
         break;

     case AfxSig_bb:      //  AfxSig_bb, AfxSig_bw, AfxSig_bh
        lResult = ( this->*mmf.pfn_bb)((BOOL)wParam);
         break;

     case AfxSig_bWww:    //  really AfxSig_bWiw
        lResult = ( this->*mmf.pfn_bWww)(CWnd::FromHandle((HWND)wParam),
            ( short)LOWORD(lParam), HIWORD(lParam));
         break;
...
}
複製代碼
複製代碼

     固然這裏只是一個小插曲而已,它只是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的函數接口:

複製代碼
複製代碼
#include <vector>
#include <iostream>
using  namespace std;

template<typename T, typename T1>
class slot
{
public:
    slot(T* pObj, void (T::*pMemberFunc)(T1))
    {
        m_pObj=pObj;
        m_pMemberFunc=pMemberFunc;
    }
     void Execute(T1 para)
    {
        (m_pObj->*m_pMemberFunc)(para);
    }
private:
    T* m_pObj;
     void (T::*m_pMemberFunc)(T1);
};

template<typename T, typename T1>
class signal
{
public:
     void bind(T* pObj, void (T::*pMemberFunc)(T1 para))
    {
        m_slots.push_back( new slot<T,T1>(pObj,pMemberFunc));
    }
    ~signal()
    {
        vector<slot<T,T1>* >::iterator ite=m_slots.begin();
         for (;ite!=m_slots.end();ite++)
        {
            delete *ite;
        }
    }
     void  operator()(T1 para)
    {
        vector<slot<T,T1>* >::iterator ite=m_slots.begin();
         for (;ite!=m_slots.end();ite++)
        {
            (*ite)->Execute(para);
        }
    }
    
private:
    vector<slot<T,T1>* > m_slots;
};

class receiver
{
public:
     void callback1( int a)
    {
        cout<< " receiver1:  "<<a<<endl;
    }
     void callback2( int a)
    {
        cout<< " receiver2:  "<<a<<endl;
    }
};


class sender
{
public:
    sender(): m_value( 0)  {}
     int get_value()
    {
         return m_value;
    }
     void set_value( int new_value)
    {
         if (new_value!=m_value)
        {
            m_value=new_value;
            m_sig(new_value);
        }
    }
    signal<receiver, int> m_sig;
private:
     int m_value;
};



int main( int argc, char** arg)
{
    receiver r;
    sender s;
    s.m_sig.bind(&r,&receiver::callback1);
    s.m_sig.bind(&r,&receiver::callback2);
    s.set_value( 1);
     return  0;
}
複製代碼
複製代碼

     程序在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就是用此手法,改進後所有代碼以下:

複製代碼
複製代碼
#include <vector>
#include <iostream>
using  namespace std;

template<typename T1>
class slotbase
{
public:
     virtual  void Execute(T1 para)= 0;
};

template<typename T,typename T1>
class slotimpl :  public slotbase<T1>
{
public:
    slotimpl(T* pObj, void (T::*pMemberFunc)(T1))
    {
        m_pObj=pObj;
        m_pMemberFunc=pMemberFunc;
    }
     virtual  void Execute(T1 para)
    {
        (m_pObj->*m_pMemberFunc)(para);
    }
private:
    T* m_pObj;
     void (T::*m_pMemberFunc)(T1);
};

template<typename T1>
class slot 
{
public:
    template<typename T>
        slot(T* pObj, void (T::*pMemberFunc)(T1)) 
    {
        m_pSlotbase= new slotimpl<T,T1>(pObj,pMemberFunc);
    }
    ~slot()
    {
        delete m_pSlotbase;
    }
     void Execute(T1 para)
    {
        m_pSlotbase->Execute(para);
    }
private:
    slotbase<T1>* m_pSlotbase;
};

template<typename T1>
class signal
{
public:
    template<typename T>
     void bind(T* pObj, void (T::*pMemberFunc)(T1 para))
    {
        m_slots.push_back( new slot<T1>(pObj,pMemberFunc));
    }
    ~signal()
    {
        vector<slot<T1>* >::iterator ite=m_slots.begin();
         for (;ite!=m_slots.end();ite++)
        {
            delete *ite;
        }
    }
     void  operator()(T1 para)
    {
        vector<slot<T1>* >::iterator ite=m_slots.begin();
         for (;ite!=m_slots.end();ite++)
        {
            (*ite)->Execute(para);
        }
    }
    
private:
    vector<slot<T1>* > m_slots;
};

#define CONNECT(sender,signal,receiver,slot)  sender.signal.bind(receiver,slot)

class receiver
{
public:
     void callback1( int a)
    {
        cout<< " receiver1:  "<<a<<endl;
    }
};
class receiver2
{
public:
     void callback2( int a)
    {
        cout<< " receiver2:  "<<a<<endl;
    }
};

class sender
{
public:
    sender(): m_value( 0)  {}
     int get_value()
    {
         return m_value;
    }
     void set_value( int new_value)
    {
         if (new_value!=m_value)
        {
            m_value=new_value;
            m_valueChanged(new_value);
        }
    }
    signal< int> m_valueChanged;
private:
     int m_value;
    
};

int main( int argc, char** arg)
{
    receiver r;
    receiver2 r2;
    sender s;
    CONNECT(s,m_valueChanged,&r,&receiver::callback1);
    CONNECT(s,m_valueChanged,&r2,&receiver2::callback2);
    s.set_value( 1);
     return  0;
}
複製代碼
複製代碼

     這個版本就比較像樣了,一個signal可與多個slots鏈接,增長了相似QT的connect,用宏實現#define CONNECT(sender,signal,receiver,slot) sender.signal.bind(receiver,slot),這樣使用者就很是方便,並且如今已徹底解耦,sender只管定義本身的signal,在恰當時機用仿函數形式調用便可,而receiver只管實現callback,互不影響,可獨立工做,若是須要再經過CONNECT將它們鏈接起來便可,已經很組件化了,但是離真正的工程應用尚有一段距離,如它不能接收全局函數或靜態成員函數或仿函數爲回調函數,不能帶兩個或更多的函數參數,最後一步了。

相關文章
相關標籤/搜索