[分享] Signal & Slot 的變形作法

看板C_and_CPP作者 (meow)時間13年前 (2012/07/13 08:52), 編輯推噓2(206)
留言8則, 4人參與, 最新討論串1/1
Qt玩了一陣子 體會到Signal & Slot的方便之處 但總覺得那些擴充語法很不自然 後來去看了Boost的實現 也覺得不是很直觀 於是我自己嘗試寫了一個簡陋的替代品 試圖輕巧地實現類似Signal & Slot的功能 想跟大家分享一下這個想法 先直接來看成果好了: #include "publisher.h" //下面兩個類別是我要拿來測試的工具 //實做我就省略不寫了 class Class1 { public: void fnc(); //印出類別名稱和一個沒啥意義的字串 void fnc_A(int n); //印出類別名稱 函數名稱 和一個整數 void fnc_B(int n); //印出類別名稱 函數名稱 和一個整數 void fnc_C(int n, int n1); //印出類別名稱 函數名稱 和兩個整數 void fnc_D(int n, int n1); //印出類別名稱 函數名稱 和兩個整數 }; class Class2 { public: void fnc(); //印出類別名稱和一個沒啥意義的字串 void fnc_A(int n); //印出類別名稱 函數名稱 和一個整數 void fnc_B(int n); //印出類別名稱 函數名稱 和一個整數 void fnc_C(int n, int n1); //印出類別名稱 函數名稱 和兩個整數 void fnc_D(int n, int n1); //印出類別名稱 函數名稱 和兩個整數 }; int main() { Class1 object1; Class2 object2; //一個替代 Signal 的仿函式,相當於宣告 void publish(); Publisher<> publish; publish.connect(object1, &Class1::fnc); //連接槽函數 publish.connect(object2, &Class2::fnc); //連接槽函數 publish(); //發出訊號喚醒所有連接的槽函數 //一個替代 Signal 的仿函式,相當於宣告 void publish_1_int(int); Publisher<int> publish_1_int; publish_1_int.connect(object1, &Class1::fnc_A); //連接槽函數 publish_1_int.connect(object1, &Class1::fnc_B); //連接槽函數 publish_1_int.connect(object2, &Class2::fnc_A); //連接槽函數 publish_1_int.connect(object2, &Class2::fnc_B); //連接槽函數 publish_1_int(99); //發出訊號喚醒所有連接的槽函數 //一個替代 Signal 的仿函式,相當於宣告 void publish_2_int(int, int); Publisher<int, int> publish_2_int; publish_2_int.connect(object1, &Class1::fnc_C); //連接槽函數 publish_2_int.connect(object1, &Class1::fnc_D); //連接槽函數 publish_2_int.connect(object2, &Class2::fnc_C); //連接槽函數 publish_2_int.connect(object2, &Class2::fnc_D); //連接槽函數 publish_2_int(99, 17); //發出訊號喚醒所有連接的槽函數 //建立另一個訊號,型別與 publish 匹配 Publisher<> publish_by_others; publish_by_others.connect(publish); //連接一個匹配的訊號 publish_by_others(); //發出訊號喚醒另一個訊號 //建立另一個訊號,型別與 publish_2_int 匹配 Publisher<int, int> publish_2_int_by_others; publish_2_int_by_others.connect(publish_2_int); //連接一個匹配的訊號 publish_2_int_by_others(86, 44); //發出訊號喚醒另一個訊號 return 0; } 執行結果如下:https://www.dropbox.com/s/tuk2vulmah0vob1/publisher_demo.JPG
看完結果 或許各位對我做出來的東西如何運作已經有一些概念了(真的嗎?......XD) ============================================================================== 接下來討論作法: Publisher 是一個使用 template 宣告的仿函式 目前的版本中,它有8個型別參數(本來想寫16個,但太麻煩了) 每個型別參數都預設為一個沒有定義的空頭型別 class NullType; (關於這個手法,詳見 Modern C++ Design 一書) 接下來只要使用偏特化技巧,就可以做出類似 template overloading 的效果 這樣我們就解決了 Signal functor 的參數不固定的問題 Publisher 的內容非常簡單 裡面以兩個 stack 分別儲存 Connection 和 Publisher 兩種物件的指標 這樣我們就可以依序調用這些指標喚起其他函數 調用 Connection 指標是喚起其他物件裡面的 public member function 調用 Publisher 指標是喚起另一個訊號 以單一參數的 Publisher 為例 實作碼如下: template <typename T> class Publisher<T, NullType, NullType, NullType, NullType, NullType, NullType, NullType> { public: ~Publisher(); //將 stack 中儲存的 Connection1 都 delete 掉 //下面之所以要重載connect函數是為了對付連接訊號與連接槽函數兩種情況 template <class S> void connect(S &subscriber, void (S::*fnc)(T)); void connect(Publisher<T> &publisher) {publishers_.add_item(&publisher);} void operator ()(T arg); //依序調用指標執行目標函數 private: //Stack 大家都會寫我就不講了,懶得自己做的人就用標準函式庫吧^^ MyNameSpace::Stack<Connection1<T> *> connections_; MyNameSpace::Stack<Publisher<T> *> publishers_; }; 其他的偏特化也都類似 上面的 Publisher 物件雖然看似簡單 其中卻有個需要一點小技巧的函數,也就是 template <class S> void connect(S &subscriber, void (S::*fnc)(T)); 由於對不同類別中的成員函數而言 即使參數型別和返回型別相同 其指標型別 void (S::*fnc)(T) 還是不同 所以我們必須將擁有該成員函數的類別 S 寫成 template 參數 事實上型別 Connection1<T> * 是表示一個指向Base Class的指標 我們真正產生的物件之型別是 ConcreteConnection1<S, T> 雖然這只不過是物件導向程式設計中最基本的泛型手法 但在這時卻非常有效 使用一個純虛類別去統一管理針對所有不同 class S 的指標以及成員函數指標 就可以讓我們將不同型別的指標放在同一個 Stack 裡面 類別ConcreteConnection1<S, T>的實做沒有任何技巧 這裡就省略不寫了 反正就只是用兩個指標分別指向一個 class S 物件和該物件的一個成員函數而已 最後,附上上面那個比較麻煩的connect函數之完整實作碼: template <typename T> template <class S> void Publisher<T, NullType, NullType, NullType, NullType, NullType, NullType, NullType>::connect(S &subscriber, void (S::*fnc)(T)) { ConcreteConnection1<S, T> *connection = new ConcreteConnection1<S, T>(&subscriber, fnc); connections_.push(connection); } ============================================================================= 講完啦XD 基本上就是這樣 我覺得跟Qt比起來 這個作法有一些小小的優勢 1. 不需什麼特別的預編譯工具,只要引入一個僅有數百行的 "publisher.h" 檔案就行了 2. connect 函數的用法簡單直覺,不使用巨集,僅用標準c++語法完成工作 3. 運行效能完全正比於 connections 的數量,而且執行順序固定 4. 任何物件的 public 成員函數都有資格成為槽函數,物件設計者不需要特別宣告 5. 不需要繼承任何類別就可以使用此功能,這意謂著,可以使用我們的 Publisher 去連 接他人寫作的類別庫中的成員函數,不需要對該類別做任何改動 第一次發這麼長的文章,有興趣的人可以參考看看.....^^" -- 直接閱讀《琴劍六記》 http://gs.cathargraph.com/p/list.html   《琴劍六記》Facebook專頁 https://www.facebook.com/GSannals -- ※ 發信站: 批踢踢實業坊(ptt.cc) ◆ From: 219.85.215.210 ※ 編輯: pnpncat 來自: 219.85.215.210 (07/13 16:59) ※ 編輯: pnpncat 來自: 219.85.215.210 (07/13 17:03) ※ 編輯: pnpncat 來自: 219.85.215.210 (07/13 17:05)

07/13 17:10, , 1F
附帶提一下 之所以叫做 Publisher 而不叫 Signal
07/13 17:10, 1F

07/13 17:11, , 2F
是因為我昨天在寫時使用 QtCreater 當作 IDE 那個字不能
07/13 17:11, 2F

07/13 17:11, , 3F
亂用......XD
07/13 17:11, 3F

07/14 11:33, , 4F
good
07/14 11:33, 4F

07/14 16:27, , 5F
用 Variadic tempalte + std::forward 會簡化不少
07/14 16:27, 5F

07/14 23:30, , 6F
我還沒跟上c++11 .......XD
07/14 23:30, 6F

07/18 17:11, , 7F
可以參考這個lib http://sigslot.sourceforge.net/
07/18 17:11, 7F

07/19 02:19, , 8F
http://ppt.cc/xue- 這個更有趣^^
07/19 02:19, 8F
文章代碼(AID): #1F_-7Rd0 (C_and_CPP)