標籤:訊號槽 qt sigslot 回調
我們從一個具體的問題入手:
“現在有一堆按鈕,以及一堆電器,按鈕對它需要控制的對象一無所知,電器也不知道它們開關的具體類型,它們之間的關係可能是一對多,也可能是多對一,並且需要支援動態添加和刪除,應該如何設計這個結構?”
這裡有個形象的圖:
為了實現組件間的控制,我們很容易想到“回呼函數”,對於C++開發人員,我們肯定不希望一個類自身的處理函數存在於類外,但是類成員函數中被自動添加的隱形this形參造成了函數指標調用的不匹配,於是我們想到了使用static成員函數:
// 被調類class Tv{public: static void onBtnClicked(bool b) { if ( b == true ) cout << "Tv is being turned on."; else cout << "Tv is being turned off."; }};// 還有其他可能被調用的對象// class Lamp...// class Laptop...typedef void (*PF)(bool);class Button{public: //主調函數 void click(PF p, bool b) { p(b); }};int main(){ PF p = &Tv::onBtnClicked; Button btn; btn.click(p, true); return 0;}
這樣我們就可以動態地為Button賦上它需要調用那個函數並執行。但缺點也是顯而易見的——被調函數作為靜態函數不能訪問非靜態成員與函數,Button觸發的結果對於每一個Tv對象都是一樣的,這顯然不是我們想要的。因此,我們想到了在Button中動態地傳入被調對象的指標,這樣,通過這個指標我們就可以調用該對象的非靜態成員函數了。由於Button還需要控制其他裝置,我們很自然地想到使用模板:
//被調類class Tv{public: Tv(int t1, int t2) : bootTime(t1), offTime(t2){} //被調函數 void onBtnClicked(bool b) { if ( b == true ) cout << "Tv is being turned on. bootTime is " << bootTime; else cout << "Tv is being turned off. offTime is " << offTime; }private: int bootTime; int offTime;};// 還有其他可能被 Button 類控制的被調類class Lamp{public: void onBtnClicked() { cout << "This Lamp is control by voice"; }};// class Laptop...// 主調類template<typename Tobject, typename Tparam>class Button{ typedef void (Tobject::*Tfunc)(Tparam);public: Button(Tobject* pInstance, Tfunc p) { m_pInstance = pInstance; m_pf = p; } //主調函數 void click(Tparam p) { (m_pInstance->*m_pf)(p); }private: Tfunc m_pf; Tobject* m_pInstance;};int main(){ Tv tv1(20, 40); Button<Tv, bool> btn(&tv1, &Tv::onBtnClicked); btn.click(true); return 0;}
這樣看起來還不錯,我們不僅將被調對象的類型獨立於Button,並且參數類型也是可變的(當然為了滿足一對多的關係,Button應該維護Tobject*和Tfunc的List。)。但我們依然沒有做到完全解耦:一、對於Button類中聲明的函數指標( typedef void (Tobject::*Tfunc)(Tparam) ),被調函數的傳回值與參數數量必須與其完全一致,但多個類型的被調函數可能不盡相同,例如Lamp::onBtnClicked();二、Button類掌握著所有被調對象的指標,這並不是一件安全的事情;三、被調對象先於調用被析構,將得到錯誤的結果。我們可以將main()中代碼改成下面這樣:
int main(){ Tv *tv1 = new Tv(20, 40); Button<Tv, bool> btn(tv1, &Tv::onBtnClicked); delete tv1; btn.click(true); return 0;}
危險的是,這是一個NULL指標調用成員函數的問題,很多時候編譯器並不對該情況報錯,於是我們得到了一個詭異的結果——列印了一個隨機的bootTime值。有時這種情況藏得很隱蔽,它可能將你拖入調試bug的沼澤。
之所以出現這樣的情況,是因為Tv類的完全獨立性,它沒有能力將自身被delete的資訊告訴Button,從而導致Button對其固執地調用;而Button類過多地關注被調對象與其函數指標也依然存在耦合,瞭解了問題所在,我們再來嘗試新的結構。首先我們把Button類中負責調用其他函數的內容剝離出來,用一個新的類Connection表示:
template<typename Tparam>class Connection_Base{public: virtual MySlot* getdest() const = 0; virtual void emitSignal(Tparam) = 0; virtual ~Connection_Base(){}};template<typename Tobject, typename Tparam>class Connection : public Connection_Base<Tparam>{ typedef void (Tobject::*Tfunc)(Tparam);public: Connection(Tobject* pobject, Tfunc p) { m_pobject = pobject; m_pf = p; } void emitSignal(Tparam parm) { (m_pobject->*m_pf)(parm); } MySlot* getdest() const { return m_pobject; }private: Tobject* m_pobject; Tfunc m_pf;};
注意到這裡我們使用了兩個類,Connection和它的抽象基類Connection_Base,Connection所做的工作與我們從Button類中剝離出來的差不多,同樣維護著一個對象及其函數指標。之所以要添加這個基類Connection_Base是因為我們在發出訊號時是不關心接受者的類型的,因此只瞭解參數類型的訊號對象就可以只與Connection_Base進行互動。
接下來,為了讓“電器們”在銷毀時能夠及時解除它身上所有的綁定,最直接的方式是通知所有的主調類將它刪除。我們可以設計一個父類來完成這個工作,擁有槽函數的類只需要繼承它就好:
class MySlot{private: typedef std::set<_Signal_Base*> sender_set; typedef sender_set::const_iterator const_iterator;public: void signal_connect(_Signal_Base* sender) { m_senders.insert(sender); } void signal_disconnect(_Signal_Base* sender) { m_senders.erase(sender); } virtual ~MySlot() { disconnect_all(); } void disconnect_all() { const_iterator it = m_senders.begin(); const_iterator itEnd = m_senders.end(); while(it != itEnd) { (*it)->slot_disconnect(this); ++it; } m_senders.erase(m_senders.begin(), m_senders.end()); }private: sender_set m_senders;};
即使現在我們還沒貼出_Signal_Base的代碼,相信 MySlot 這個類也依然十分容易理解,讓我們把每個訊號理解為一個對象,那麼 MySlot 則維護著一個儲存著所有當前已綁定的訊號的集合,在解構函式中我們調用disconnect_all(),調用每個訊號對象的slot_disconnect()函數移除自身。
有了連接者和槽,接下來我們就該實現Signal了:
class _Signal_Base{public: virtual void slot_disconnect(MySlot* pslot) = 0;};
template<typename Tparam>class Signal_Base : public _Signal_Base{public: typedef std::list<Connection_Base<Tparam>*> connections_list; ~Signal_Base() { disconnect_all(); } void disconnect_all() { typename connections_list::const_iterator it = m_connected_slots.begin(); typename connections_list::const_iterator itEnd = m_connected_slots.end(); while(it != itEnd) { (*it)->getdest()->signal_disconnect(this); delete *it; ++it; } m_connected_slots.erase(m_connected_slots.begin(), m_connected_slots.end()); } void disconnect(MySlot* pclass) { typename connections_list::iterator it = m_connected_slots.begin(); typename connections_list::iterator itEnd = m_connected_slots.end(); while(it != itEnd) { if((*it)->getdest() == pclass) { delete *it; m_connected_slots.erase(it); pclass->signal_disconnect(this); return; } ++it; } } void slot_disconnect(MySlot* pslot) { typename connections_list::iterator it = m_connected_slots.begin(); typename connections_list::iterator itEnd = m_connected_slots.end(); while(it != itEnd) { typename connections_list::iterator itNext = it; ++itNext; if((*it)->getdest() == pslot) { m_connected_slots.erase(it); //delete *it; } it = itNext; } }protected: connections_list m_connected_slots;};
Signal_Base類維護了一個Connection的列表,由於每個新的連結都基於一個Connection,通過Connection的getdest()函數可以確定該連結綁定在哪個對象上,這樣,刪除某個Connection就可以刪除特定的連結了。最後的MySignal類是醬紫的:
template<typename Tparam>class MySignal : public Signal_Base<Tparam>{public: template<typename Tobject> void connect(Tobject* pclass, void (Tobject::*pmemfun)(Tparam)) { Connection<Tobject, Tparam>* conn = new Connection<Tobject, Tparam>(pclass, pmemfun); Signal_Base<Tparam>::m_connected_slots.push_back(conn); pclass->signal_connect(this); } void operator()(Tparam p) { typename std::list<Connection_Base<Tparam>*>::const_iterator itNext, it = Signal_Base<Tparam>::m_connected_slots.begin(); typename std::list<Connection_Base<Tparam>*>::const_iterator itEnd = Signal_Base<Tparam>::m_connected_slots.end(); while(it != itEnd) { itNext = it; ++itNext; (*it)->emitSignal(p); it = itNext; } }};
MySignal提供了connect()函數用來為新的連結產生Connection對象,然後重載了operator()使得我們可以使用“signal()”這種類似函數的形式來發出訊號。
一切就緒,寫段測試代碼看看:
class Button{public: MySignal<bool> click; void nowClick(bool b) { click(b); }};class Tv : public MySlot{public: Tv(int i) : m_id(i){} void onButtonClicked(bool b) { if ( b == true ) cout << "Tv " << m_id << " is being turned on. " << endl; else cout << "Tv "<< m_id <<" is being turned off. " << endl; }private: int m_id;};int main(){ Button btn; Tv *tv1 = new Tv(1); Tv *tv2 = new Tv(2); btn.click.connect(tv1, &Tv::onButtonClicked); btn.click.connect(tv2, &Tv::onButtonClicked); delete tv1; btn.nowClick(false); return 0;}
可以看到,Button類和Tv類現在可以徹底不再相互關心,而連結的工作交給客戶程式(main)去做,同時,在delete tv1之後,我們也不會碰到異常。完整的代碼實際就是把上面這幾塊揉在一起,就不再貼了,有興趣的話可以到博主的百度網盤下載:http://pan.baidu.com/s/1i3F8gkd
可能你已經注意到了,如果我們的訊號有兩個參數呢?是的,我們需要再寫一個有兩個參數的Signal_Base,Connection和MySignal。。。如果我們分別為0-8個參數寫了9個版本,並為這其中的容器操作加上安全執行緒機制,那麼我們現在完成的就是一份sigslot庫。
不知不覺篇幅有點長了~我們就留到下篇再來探討下,Qt中訊號槽的實現與sigslot又有哪些差異?以及moc和QObject又給訊號槽機制帶來了怎樣的靈活性~
著作權聲明:本文為博主原創文章,未經博主允許不得轉載。
從零開始實現訊號槽機制:一