憋了很長一段時間的想法,在這裡說說,希望聽聽諸位高手的意見。
我寫過不少C++程序(當然比起高手還是差遠了),寫過庫也寫過客戶程序。一般庫都會提供一些好用的類供客戶程序使用,不少庫還可以讓客戶程序響應庫內的某些事件。比如MFC/ATL/VCL提供消息響應,很多ActiveX提供自定義消息響應,甚至是系統底層的中斷調用都可以列入這個范疇。然而,正是以上這些“反向”的調用讓我覺得很煩惱。
1 繼承+多態乍一看是理所當然的選擇,庫中的類把響應處理函數設置為虛函數,客戶程序可以繼承這個類並且重載響應函數。以某個Socket類為例,可以提供一個OnRecv函數用來響應網絡數據包到達的處理。客戶程序只需要重載OnRecv並進行自己的處理就可以了。
struct Socket { // base class
virtual void OnRecv();
};
stuct MySocket { // your event-handle class
virtual void OnRecv() { /* do sth here ... */ }
}
疑問:很多時候這樣做實在很煩,特別是做小程序的時候,或者需要快速做原型的時候,一眼望去小小的程序一上來就繼承了一大堆東西,頗為不爽。只是想著能省事一點,希望能像那些腳本語言一樣快速綁定消息響應,而不是以繼承開始工作——我已經害怕看到長長的類繼承樹了,很多時候根本不必要繼承整個類;又或者某些類只提供一個接口而不是具體的類又或者需要多重繼承,處理都有一定麻煩;最麻煩的莫過於有時候需要改變響應處理,難道繼承好幾個下來麼——這麼多虛表也是浪費啊。
wangtianxing老大點評:為了使用Socket就必須繼承Socket,這可以說是Socket的設計的問題。如果需要實現類似的功能的話,可以寫成如下,雖然和繼承 Socket 沒有多少本質的差別,不過確實把消息處理類和Socket的實現扯開了。:
struct SocketEventHandler {
virtual void OnRecv() { /* ... */ }
virtual void OnSend() { /* ... */ }
};
struct Socket {
void set_handler( SocketEventHandler* h ) { handler_ = h; }
private:
SocketEventHandler* handler_;
};
struct MyHandler : SocketEventHandler {
void OnRecv() { ... }
};
Socket s;
MyHandler h;
s.set_handler( &h );
突然之間,我感到一陣迷茫,非常渴望一種簡單明確的表達方法。丟開繼承,我們還有什麼把戲?我不禁想起了c時代的回調函數……
2 回調函數(CallBack)非常簡單,就是一個函數指針。剛才的OnRecv可以寫成這樣
struct Socket {
void OnRecv() { if(OnRecvHandle!=NULL) OnRecvHandle(); }
void (*OnRecvHandle) ();
};
客戶程序只需要編寫一個MyOnRecv函數,並且賦值給OnRecvHandle就可以了
void MyOnRecv(); // your event-handle function
Socket foo;
foo.OnRecvHandle = MyOnRecv;
疑問:非常簡單,不需要繼承類就可以處理,而且隨時可以替換不同的處理函數。其實多態的本質也是函數指針,只不過多態是用vtable統一管理函數指針。回調函數要特別注意函數指針是否為空的問題,因此最好外面在包裝一層判斷過程。回調函數最大問題在於類型不安全,顯式指針這東西……不說也罷……翻了一下智能指針和模版,我發現了一根稻草……
3 委托(Delegation)委托是什麼冬冬?這個名詞似乎是時尚的代名詞,我仿佛看到學java/c#的兄弟們在嘲笑我們的落後……其實,property不也可以算是一種委托嗎?說白了不就是智能指針麼?
我覺得委托最本質的是提供一種類型安全的動態消息響應轉移機制。
以前,我對委托一無所知,我覺得無非就是一個類型安全的智能指針,而所謂的Multi-Cast Delegation無非就是一個智能指針數祖……是不是還有Any-Cast Delegation呢?我不知道,也許有吧,無非就是智能指針數祖+隨機數發生器……
但是,實際上並不是那麼簡單。你可以把我剛才說的函數指針封裝一下弄一個類封裝起來,不過,這直接導致某個消息的響應只能是固定死的函數指針類型,甚至不能是可愛的Functor或者是某個類的成員函數。你可能會跟我抬槓說這怎麼可能,不是可以用template實現麼?我們來看一個例子
假設某個委托類 Dummy_Delegation 擁有一個成員函數用來連接處理函數 template<class T> void Dummy_Delegation::Connect(T _F); 沒錯,_F可以不一定函數指針,也可以是Functor,我們利用_F()來呼叫響應函數,一切看起來是多麼美好——但是,很不幸,這個_F無法保存下來供消息產生的時候呼叫……
一切都因為這個該死的template<class T>,你無法在Dummy_Delegation內定義一個T類型的變量或者指針來保存_F。退一萬步說,你把T作為整個Dummy的模版,還是避免不了在模版實例化的時候定死類型。於是,整個Delegation的通用性大打折扣……
實際上,我們希望有這麼一種Delegation,他可以把消息響應動態綁定到任何一個類的成員函數上只要函數類型一致。注意,這裡說的是任何一個類。這就要求我們屏蔽信號發生器和響應類之間的耦合關系,即,讓他們相互都不知道對方是誰甚至不知道對方的類型信息。
這個方法可行麼?Yes!
4 橋式委托(Bridge Delegation) ---- 利用泛型+多態來實現請允許我杜撰一個名詞:橋式委托(Bridge Delegation)
實現這麼一個東西真的很有意思,其實,像gtk+/qt很多需要"信號/反饋"(signal/slot)的系統都是這麼實現的。
說到GP和Template,那真的可以算是百家爭鳴了,就像boost和loki還在爭奪新的C++標准智能指針的地位打得不可開交。而Functor這個東西有是很多GP algo的基礎,比如sort/for_each等等。
整個橋式委托的結構如下圖:
Signal <>-------->* Interface
^
|
Implementation<Receiver> -------------> Receiver
我們搭建了一個Interface/Implementation的橋用來連接Singal和Receiver,這樣就
可以有效隔開雙方的直接耦合。用之前我們的Socket類來演示如下:
struct Socket {
Signal OnRecv;
};
一個Receiver可以是一個function比如 void OnRecv1() 也可以是一個Functor:
struct OnRecv2_t {
void operator() ();
} OnRecv2;
我們可以這樣使用這個橋式委托
Socket x;
x.OnRecv.ConnectSlot(OnRecv1); //或者 x.OnRecv.ConnectSlot(OnRecv2());
當消息產生調用 x.OnRecv()的時候,用戶指定的OnRecv1或者OnRecv2就會響應
我們來看看如何實現這個橋:首先是一個抽象類
struct DelegationInterface {
virtual ~DelegationInterface() {};
virtual void Action() = 0;
};
然後才是模版類Impl:
template<class T>
struct DelegationImpl : public DelegationInterface {
T _FO;
DelegationImpl(T _S) :_FO(_S) { }
virtual void Action() { _FO(); }
};
注意我們上面的圖示,這個DelegationImpl類是跟Receiver相關聯的,也就是說這個Impl類知道所有的Receiver細節,於是他可以從容地調用Receiver()。再次留意這個繼承關系,對了,一個virutal的Action函數!利用多態性質,我們可以根據Receiver來實例化DelegationImpl類,卻可以利用提供一致的訪問Action的Interface,這就是整座橋的秘密所在——利用多態下層隔離細節!
再看看我們的Signal類:
struct Signal {
DelegationInterface* _PI;
Signal() :_PI(NULL) {}
~Signal() { delete _PI; }
void operator()() { if(_PI) _PI->Action(); }
template<class T> void ConnectSlot(T Slot) {
delete _PI; _PI = new DelegationImpl<T>(Slot);
}
};
顯然,Signal類利用了 DelegationInterface* 指針_PI來呼叫響應函數。而完成這一切連接操作的正是這個奇妙的ConnectSlot的函數。對了!上次討論模版函數的時候就說了這個T類型無法保存,但是這裡用橋避開了這個問題。利用模版函數的T做為DelegationImpl的實例化參數,一切就這麼簡單地解決了~
你也許可能會抗議,認為我繞了一大圈又繞回了一開始我煩惱的繼承/多態上面來了。呵呵。其實,你有沒有發現,我們這個Singal/Bridge Delegation/Receive的體系是固定的一套東西,你在實際使用中並不需要自己去繼承去處理重載,你只需要好好地Connect到正確的Slot就可以了。這也可以算是一種局部隱含的繼承吧。
接下來我們要討論一下這個橋式委托的性能消耗以及擴展和局限性問題
5 橋式委托的進一步研究
看過上面的橋式委托之後,可能會有點懷疑他的性能,需要一個interface指針一個functor類/函數指針,調用的時候需要一次查vtable,然後再一次做operator()調用。其實,這些消耗都不算很大的,整個橋式委托的類結構是簡單的,相對於前面說的繼承整個類之類的做法開銷還是比較小的,而且又比函數指針通用而且類型安全。最重要的是,剛才的Signal可以方便地改寫為Multi-Cast Delegation即一個信號引發多個響應——把Singal內部的DelegationInterface*指針改為一個指針隊列就可以了;-)
不過,我們剛才實現的橋式委托只能接收函數指針和functor,不能接收另外一個類的成員函數,有時候這是非常有用的動作。比如設置一個按鈕Button的OnClick事件的響應為一個MsgBox的Show方法。當然,MsgBox還有其他非常多的方法,這樣就可以不用局限於把MsgBox當成一個functor了。
我們要改寫剛才的整個橋來實現這個功能,在這裡需要你對指向成員函數得指針有所了解。
// 新版的橋式委托,可以接收類的成員函數作為響應
struct DelegationInterface {
virtual ~DelegationInterface() {};
virtual void Run() = 0;
};
template<class T>
struct DelegationImpl : public DelegationInterface {
typedef void (T::* _pF_t)(); // 指向類T成員函數的指針類型
DelegationImpl(T* _PP, _pF_t pF) :_P(_PP), _PF(pF) {}
virtual void Run() {
if(_P) { (_P->*_PF)(); } // 成員函數調用,很別扭的寫法(_P->*_PF)();
}
T* _P; // Receiver類
_pF_t _PF; // 指向Receiver類的某個成員函數
};
struct Signal
{
DelegationInterface* _PI;
Signal() :_PI(NULL) {}
void operator() () { if(_PI) _PI->Run(); }
// 新的ConnectSlot需要指定一個類以及這個類的某個成員函數
template<class T>
void ConnectSlot(T& recv, void (T::* pF)()) { // pF這個參數真夠別扭的
_PI = new DelegationImpl<T>(&recv, pF);
}
};
注意:ConnectSlot方法的pF參數類型非常復雜,也可以簡化如下,即把這個類型檢測推到DelegationImpl類去完成,而不在Connect這裡進行麼?編譯器可以正確識別。對於模板來說,很多復雜的參數類型都可以用一個簡單的類型代替,不用關心細節,就象上面用一個F代替void (T::*)()。有時候能改善可讀性,有時候象反。
template<class T, class F>
void ConnectSlot( T& recv, F pF ) {
PI_ = new DelegationImpl<T>(&recv,pF);
}
哈哈,這個新版怎麼用呢,很簡單的。比如你的MsgBox類有一個成員函數Show,你可以把這個作為響應函數:
MsgBox box;
Socket x; // Socket還跟舊的版本一樣
x.OnRecv.ConnectSlot(box, &MsgBox::Show);
注意上面這裡引用成員函數指針的寫法,一定不能寫成box.Show,呵呵,希望你還記得成員函數是屬於類公共的東西,不是某個實例的私有產品。大家不妨進一步動一下腦筋,把新版的Signal和舊版的Signal結合一下,你就可以獲得一個功能超強的Delegation系統了。
wangtianxing老大點評:用signal的辦法確實可以方便地動態替換處理函數,不過這是以每個可能被處理的消息都要在每個對象中占用一個 signal 的空間為代價的。而且,需要動態改變處理函數的應用我已經不記得什麼時候見過了。即使有,也可以通過在override的virtual函數裡自己處理實現,雖說麻煩,但也是可能的。此外,以上代碼並不夠規范,下劃線加大寫字母開頭的標識符是保留給語言的實現用的。
6 結論好了,我們關於橋式委托的討論接近尾聲了,大家也許已經發現了一個巨大的問題:上面的橋式委托無法給相應操作傳遞參數!!!是的,這是一個巨大的矛盾——你必須自己實現帶一個參數的橋、自己實現帶2個參數的橋……就像stl的functor一樣,你無法做到參數通用處理,必須區分unary_functor、binary_functor……你不得不這麼做:
template<class P1>
struct DelegationInterface { virtual void Run(P1 param) = 0; };
template<class T, class P1>
struct DelegationImpl : public DelegationInterface<P1> {
......
}
template<class P1>
struct Signal {
DelegationInterface<P1> *_PI;
......
}
哇~~~ 好悲慘! 要自己寫這麼多橋啊?C++語法這麼不給面子……當然了,你可以繞路來實現,比如用一個通用的打包參數來包裝多個參數,用宏定義來處理各種情況,當然也可以用預處理來實現——我這裡要說的,同情一下QT吧,不要整天抱怨他的signal/slot體系需要預處理是在擴展語言——設身處地地想一想,C++提供給我們的就這有這些了,一個小小的參數是我們這些signal/slot抹不去的傷痛。
幸運的是,在C++標准委員會不斷的努力之下,這些情況開始有所改善。boost庫之中的signal庫可以直接支持可變參數的委托;同時,越來越多的元語言技術也引入了C++之中。雖然目前支持這些新特性的編譯器還比較少,不過這已經是非常巨大的進步了,讓我們期待吧……