對於博客,從來是默默的轉,最近終於有點時間可以寫些自己的東西(其實也還要忙着准備找工呢),寫出來與僅僅知道那么回事絕對大不一樣,寫也是學習的過程,不過向來多關注於功能實現,對一些基礎概念把握不准確,望各位拍磚,不廢話了。
什么是回調?通常發生在需要兩個角色即調用者與實現者的情形上,即我們希望當產生某個事件時,調用實現者定義的某個函數。當然這個概念很大,不是說操作系統的信號量,條件變量什么的,是在語言級別實現,如一個Framework提供商,規定了整個程序的框架,可能產生某事件時它希望調用某個行為,而這個行為的具體定義是由framework客戶來完成。
我們從簡單的做起,通過一個個為什么最終來獲得一個比較好的回調實現。
C語言中用全局函數實現回調最簡單了:
{
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);
}
相信不用多解釋了吧, 可是到了面向對象的世界里,就不是那么簡單了,如果回調函數是成員函數怎么辦?
非靜態成員函數作回調函數
當然如果是靜態成員函數就好辦跟全局函數是類似,到此為止世界還沒有變亂,如在VC編程中用AfxBeginThread開啟一個線程,就經常將參數AFX_THREADPROC pfnThreadProc定義為一個全局函數或靜態成員函數,可是這兩個都不方便訪問類的非靜態成員,之所以鄭重其事地寫這篇文章,就是以前靜態回調用起來非常不爽。
回調函數是非靜態成員函數呢?我們可不能簡單地設為這樣:
{
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);
}
這樣編譯就不會通過的,因為非靜態的成員函數必須通過對象來訪問,好,我們稍稍改進一下:
{
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多傳個對象進去,好吧,貌似問題解決了,可是,調用者(如庫的提供商)只知道回調函數接口長這樣而已,事先全然不知客戶的類是如何定義,終於模板登上場了:
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成員,即真正的消息入口地址,
{
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進行類型轉換:
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 <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 <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將它們連接起來即可,已經很組件化了,可是離真正的工程應用尚有一段距離,如它不能接收全局函數或靜態成員函數或仿函數為回調函數,不能帶兩個或更多的函數參數,最后一步了。
boost之function
《參考》
1、http://hi.baidu.com/neil_danky/item/e4b1010c2a0af3cf905718d4
2、http://hi.baidu.com/neil_danky/item/98177ecd4f76e43398b498d5
3、http://www.ibm.com/developerworks/cn/linux/guitoolkit/qt/signal-slot/
4、boost pdf http://sourceforge.net/projects/boost/files/latest/download?source=files 之function signal
5、http://www.verycd.com/topics/2889271/ 之delegate,by 管景偉 http://weibo.com/u/1747895411?from=otherprofile&wvr=3.6&loc=infweihao