[分享] Signal & Slot 的變形作法
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
07/13 17:10, 1F
→
07/13 17:11, , 2F
07/13 17:11, 2F
→
07/13 17:11, , 3F
07/13 17:11, 3F
推
07/14 11:33, , 4F
07/14 11:33, 4F
→
07/14 16:27, , 5F
07/14 16:27, 5F
→
07/14 23:30, , 6F
07/14 23:30, 6F
推
07/18 17:11, , 7F
07/18 17:11, 7F
→
07/19 02:19, , 8F
07/19 02:19, 8F