Boost 的事件管理架構:Signal / Slot

Boost 的事件管理架構:Signal / Slot

分类: window编程 Linux编程   64人阅读  评论(0)  收藏  举报
 

Boost 的事件管理架構:Signal / Slot(上)

这篇文章写的很好,但国内需要翻墙才能看到,故转载至此,方便大家

转载:http://kheresy.wordpress.com/2011/04/07/boost_signals_part1/

 

隔了很久了,不過這篇也是之前 Boost C++ Libraries 系列文章的一部分;而這一篇要介紹的,則是 Boost 裡面的事件(event)管理函式庫 Signals2(官方頁面)。

有 Signals2 就代表有 Signals1(官方頁面),Boost 裡的這兩個函式庫基本上要做的事情是一樣的,不同的地方在於 Signals2 是設計成 thread-safe 的,而且也做了一定程度的自動連線管理,理論上在使用上會比較方便、安全,所以 Heresy 在這邊就僅就 Signals2 來做介紹了。

概念

Signals2 這個函式庫,是採用 signals / slots 的概念(最早應該是 Qt 所提出來的,參考維基百科),來實作一個 publisher、或稱事件(event)系統,也類似所謂的「委派」(delegates)的概念,功能比一般的 callback function 強了不少。

Signals / slots 這種系統的基本概念,基本上就是每一個 signal 可以連接(connect)一個、或多個 slot,而當程式發送(emit)這個 signal 的時候,所有連接到這個 signal 的 slot,就都會被呼叫並執行。

這樣設計的好處,是在於在這些 signal 和 slot 的連結,可以在 runtime 時建立,而非寫死在程式中;所以程式在編寫時,也不必去管到底他要呼叫多少、那些東西,只要去發送 signal 就好了。而這個 signal 連結到多少 slot,在程式內部也都不需要去在乎,除了可以將程式內的模組進一步切割、減少直接使用,也算是相當有彈性的寫法。


基本使用

而 Boost 的 signals2 要怎麼使用呢?有興趣的人可以直接去看 Boost 提供的教學網頁,裡面有各種狀況下的使用方法,而在範例的頁面,也有提供了數個範例程式,可以作為參考。Heresy 在這邊,則是就 Heresy 自己覺得應該比較用的到的部分,做一些簡單的說明。

首先,先來個最簡單的範例程式:

[cpp]  view plain copy
  1. // include STL headers  
  2. #include <stdlib.h>  
  3. #include <iostream>  
  4.   
  5. // include Boost header  
  6. #include <boost/signals2/signal.hpp>  
  7.   
  8. // slot function  
  9. void slotTest1( int a )  
  10. {  
  11.   std::cout << "Test1 get " << a << std::endl;  
  12. }  
  13.   
  14. void slotTest2( int a )  
  15. {  
  16.   std::cout << "Test2 get " << a << std::endl;  
  17. }  
  18.   
  19. int main( int argc, char** argv )  
  20. {  
  21.   // create a signal  
  22.   boost::signals2::signal<void (int)> mSignal1;  
  23.   
  24.   // connect signal and slot  
  25.   mSignal1.connect( slotTest1 );  
  26.   mSignal1.connect( slotTest2 );  
  27.   
  28.   // emit signal  
  29.   mSignal1( 10 );  
  30.   
  31.   return 0;  
  32. }  


首先,要使用 Boost 的 signals2,就需要 include 他的 header 檔,也就是「boost/signals2/signal.hpp」這個檔案;而裡面要用的類型,都會在boost::signals2 這個 namespace 下。

接下來,這邊定義了名為 slotTest1() 和 slotTest2() 這兩個函式,當作測試用的 slot。

在主程式裡要使用 signal / slot,要先建立所需要的 signal,在這邊就是 mSignal1;在 Boost 的 signals2 裡,每一個 signals 都會是一個型別為 boost::signals2::signal 的物件(這個和 Qt 裡不太一樣)。而這個型別是 template 的,還必須要指定他的傳入、回傳參數,在這邊就是「void (int)」,代表他是傳入一個int、不會回傳任何值;這樣的寫法和之前介紹過的 TR1 function object 是相同的(參考)。

而在建立了 signal 的物件後,就可以透過他的 connect() 函式,把這個 signal 和之前定義的 slotTest1()和 slotTest2() 這兩個 slot 函式做連結了~

要怎麼 emit 這個 signal 呢?很簡單,只要把這個 signal 的物件(mSignal1)當作 function object 來執行、並傳入參數就可以了~像以上面的程式來說,在執行「mSignal1( 10 )」之後,就會依照 connect 的順序、執行 slotTest1() 和 slotTest2() 這兩個函式,所以輸出結果會是:

Test1 get 10
Test2 get 10

當然,這邊所定義的 slot function 也可以用 class 形式的 function object 來取代(參考),例如定義一個含有對應的operator() 的 CTestSlot 的類別如下:

[cpp]  view plain copy
  1. class CTestSlot  
  2. {  
  3. public:  
  4.   void operator() ( int x )  
  5.   {  
  6.     std::cout << "CTestSlot get " << x << std::endl;  
  7.   }  
  8. };  


然後再透過 mSignal1.connect( CTestSlot() ); 來做連接,這樣也是可以的。

 

預設的 signal 回傳值

在上面的例子裡面,slot 是沒有回傳值的,那如果遇到有需要回傳值的時候,要怎麼處理呢?在預設、沒有特別處理的狀況下,signal 會將最後一個 slot 函式所回傳的值,當作整個 signal 執行後的回傳值傳回來;像以官方的範例來說,他的程式碼是:

[cpp]  view plain copy
  1. // include STL headers  
  2. #include <stdlib.h>  
  3. #include <iostream>  
  4.   
  5. // include Boost header  
  6. #include <boost/signals2/signal.hpp>  
  7.   
  8. // slot function  
  9. float product(float x, float y) { return x * y; }  
  10. float quotient(float x, float y) { return x / y; }  
  11. float sum(float x, float y) { return x + y; }  
  12. float difference(float x, float y) { return x - y; }  
  13.   
  14. int main( int argc, char** argv )  
  15. {  
  16.   // create a signal  
  17.   boost::signals2::signal<float (floatfloat)> mSignal1;  
  18.   
  19.   // connect signal and slot  
  20.   mSignal1.connect( &product );  
  21.   mSignal1.connect( "ient );  
  22.   mSignal1.connect( &sum );  
  23.   mSignal1.connect( &difference );  
  24.   
  25.   // emit signal  
  26.   std::cout << *mSignal1( 10, 3 ) << std::endl;  
  27.   
  28.   return 0;  
  29. }  


而最後輸出的結果,則會是最後一個執行到的函式 difference() 的結果,也就是「7」。不過要注意的是,這邊的回傳值的型別是經過封包過的,他的型別並不是 float,而是 boost::optional<float>,要取得他真正的值,要在他前面再加個「*」。

 

自訂回傳值處理方法

那如果有需要其他的 slot 回傳的值該怎麼辦呢?Boost 的 Signals2 在這時候,有提供所謂的「combiner」可以用來處理所有 slot 的回傳值。例如在官方的範例裡,就是定義了一個名為maximum 的 struct 來作取最大值的動作,其內容如下(略作簡化):

[cpp]  view plain copy
  1. struct maximum  
  2. {  
  3.   typedef float result_type;  
  4.   
  5.   template<typename InputIterator>  
  6.   float operator()(InputIterator first, InputIterator last) const  
  7.   {  
  8.     // If there are no slots to call,  
  9.     // just return the default-constructed value  
  10.     if(first == last )  
  11.       return 0.0f;  
  12.   
  13.     float max_value = *first++;  
  14.     while( first != last )  
  15.     {  
  16.       if (max_value < *first)  
  17.         max_value = *first;  
  18.       ++first;  
  19.     }  
  20.   
  21.     return max_value;  
  22.   }  
  23. };  


在這個 struct 裡,很重要的一點是,為了要讓 signal 知道他回傳值的型別,所以必須要去定義代表回傳執型別的「result_type」,這點和在使用 TR1 的 bind() 時是類似的(參考)。

而再來就是要去定義他的 operator(),它的形式基本上會是 template 的,必須要有兩個參數,一個代表起始值的 iterator、第二個則是結束值的 iterator;在上面的例子裡,就是first 和 last;這樣的設計結果,是為了讓他的操作和使用一般的 STL container 的 iterator 時是一致的。而 operator() 裡,就是針對給定的 iterator 範圍裡的值,去找到最大值了~

要怎麼用這個 maximum 呢?Combiner 對 signal 來說,也是透過 template 來控制的,所以要指定 combiner,就是在建立 signal 就要指定的~它的使用方法如下:

[cpp]  view plain copy
  1. // create a signal  
  2. boost::signals2::signal<float (floatfloat), maximum > mSignal1;  
  3.   
  4. // connect signal and slot  
  5. mSignal1.connect( &product );  
  6. mSignal1.connect( "ient );  
  7. mSignal1.connect( &sum );  
  8. mSignal1.connect( &difference );  
  9.   
  10. // emit signal  
  11. std::cout << mSignal1( 10, 3 ) << std::endl;  


如此一來,在 emit mSignal1 這個 signal 的時候,電腦就會透過 maximum,來取所連接的四個 slot 所回傳計算出來的結果的最大值了;在這個例子來說,結果會是 product() 的 10 * 3,也就是「30」。

另外要注意的是,由於在給定的 combiner maximum 裡已經有定義了 result_type,所以執行 signal 的operator() 的回傳值型別不會像之前一樣是 boost::optional<float>,而會直接是result_type 所代表的 float;所以在讀取他的值的時候,也不需要再加上「*」了。

而實際要使用的時候,combiner 的 result_type 也不見得要和 slot 的回傳值型別一樣,也可以定義成其他的型別;像官方範例裡面,就以 aggregate_values 做範例,將所有 slot 回傳的值,都儲存到一個 vector裡記錄下來。其程式碼如下(略作簡化):

[cpp]  view plain copy
  1. struct aggregate_values  
  2. {  
  3.   typedef std::vector<float> result_type;  
  4.   
  5.   template<typename InputIterator>  
  6.   result_type operator()(InputIterator first, InputIterator last) const  
  7.   {  
  8.     result_type values;  
  9.     while(first != last)  
  10.     {  
  11.       values.push_back(*first);  
  12.       ++first;  
  13.     }  
  14.     return values;  
  15.   }  
  16. };  


在這個例子裡,result_type 是被定義為std::vector<float>;而operator() 裡所做的事,就是單純地把所有資料都往這個 vector 裡面塞了。

而在使用時,基本上就和使用 maximum 這個 combiner 時一樣,把 mSignal1 的型別改為「boost::signals2::signal<float (floatfloat), aggregate_values >」就可以了。這樣一來,執行 mSignal1( 10, 3 ) 所得到的回傳值,就會是一個 vector<float>,裡面有四項,值則分別是四個 slot function 的回傳值,也就是 [ 30, 3.33, 13, 7 ];而之後要在做什麼處理,就隨便程式設計師了~

 

這一篇就先寫到這了。基本上對於一般的使用來說,搞不好也算夠了?之後,Heresy 會再針對 singal / slot 的連結管理,做大概的介紹。

 

Boost 的事件管理架構:Signal / Slot(中)

延續前一篇,這篇繼續介紹 Boost::Signals2 這個函式庫關於連線管理的部分。

控制 slot 的順序

前面已經有提過了,一個 signal 可以連接多個 slot,而當這個 signal 被 emit 的時候,基本上在沒做特殊調整的狀況下,slot function 被執行的順序,是會按照 connect 的順序來執行的。而如果要指定 slot function 執行的順序的話,在 Boost 的 signals2 在 connect 的時候,是有提供一些方法可以做設定的。

Signals2 在執行 slot 的時候,實際上是分三個階段來執行的:

  1. 使用 boost::signals2::at_front 連結的 slot
  2. 指定群組(group)的 slot
  3. 使用 boost::signals2::at_back 連結的 slot

其中,有指定群組的 slot 又會再根據群組的順序來執行,所以可調整的空間算是相當地大。

使用方法的部分,這邊先針對 at_front 和 at_back 這兩種連結位置(connect position)來做說明。基本上,at_front 就是往前塞、越晚連接的越先執行,而at_back 則是往後塞,越早連接的越先執行;而以 Signals2 來說,預設是使用 at_back,所以在沒有特殊定的情況下,救世會按照連接的順序來執行 slot。

要指定使用 at_front 或 at_back 當作連結位置也相當簡單,只要在執行 signal::connect() 的時候,除了傳入要連接的 slot 外,再額外加上指定的連結位置參數就可以了~下面就是一個簡單的例子:

[cpp]  view plain copy
  1. // include STL headers  
  2. #include <stdlib.h>  
  3. #include <iostream>  
  4.   
  5. // include Boost header  
  6. #include <boost/signals2/signal.hpp>  
  7.   
  8. // slot function  
  9. void slotFunc1(){ std::cout << "Function 1" << std::endl; }  
  10. void slotFunc2(){ std::cout << "Function 2" << std::endl; }  
  11. void slotFunc3(){ std::cout << "Function 3" << std::endl; }  
  12. void slotFunc4(){ std::cout << "Function 4" << std::endl; }  
  13. void slotFunc5(){ std::cout << "Function 5" << std::endl; }  
  14.   
  15. int main( int argc, char** argv )  
  16. {  
  17.   // create a signal  
  18.   boost::signals2::signal<void () > mSignal1;  
  19.   
  20.   // connect signal and slot  
  21.   mSignal1.connect( slotFunc1 );  
  22.   mSignal1.connect( slotFunc2, boost::signals2::at_back );  
  23.   mSignal1.connect( slotFunc3, boost::signals2::at_front );  
  24.   mSignal1.connect( slotFunc4 );  
  25.   mSignal1.connect( slotFunc5, boost::signals2::at_front );  
  26.   
  27.   // emit the signal  
  28.   mSignal1();  
  29.   
  30.   return 0;  
  31. }  


在這個範例裡,定義了五個 slot function:slotFunc1()slotFunc2()slotFunc3()slotFunc4()slotFunc5(),每一個 function 的內容,都只是輸出自己的編號而已,相當地單純。

而在連結部分的程式碼,這邊則是依序連結這五個 slot function,不過其中,slotFunc1() 和slotFunc4()是使用一般的連結方法,也就是不特別指定連結位置(也就是採用預設值 at_back);而連結 slotFunc2()的時候,雖然有強制指定為 at_back,不過實際上的意義和 1 和 4 是相同的。真正不一樣的,是在連結slotFunc3() 和 slotFunc5() 的時候,都指定了連結位置是 at_front

而這樣的執行結果,則是:

Function 5  <— front
Function 3  <— front
Function 1  <— back
Function 2  <— back
Function 4  <— back

其中,5 和 3 是 at_front 的,所以比較晚連結的 slotFunc5() 才會被第一個執行;而在這兩者執行後,才會再去執行 at_back 的 1、2、4,而這三個 slot function 就是按照連結的順序來執行了。

 

Slot 群組

除了 at_front 和 at_back 這兩種連結位置外,Boost::Signals2 也還提供了 slot 呼叫的群組功能,可以進一步地去調整 slot 被呼叫的順序。而要指定 slot 群組的方法也相當簡單,只要在呼叫signal::connect()時,在第一個參數指定群組(群組預設型別是 int)就可以了~

而這邊的範例,就針對上面的程式碼「connect signal and slot」的部分做修改,變成:

// connect signal and slot mSignal1.connect( slotFunc1 ); mSignal1.connect( 1, slotFunc2 ); mSignal1.connect( 0, slotFunc3 ); mSignal1.connect( 0, slotFunc4 ); mSignal1.connect( slotFunc5, boost::signals2::at_front );

其中,slotFunc1() 是使用預設的at_back 作為連結位置,slotFunc5() 則是使用at_front 當作連結位置;而其他的三個 slot 則是都有指定群組,其中 slotFunc2() 是指定群組「1」,slotFunc3()slotFunc4() 則是指定為群組「0」。如此一來,執行的結果就會變成是:

Function 5  <— front
Function 3  <— group 0
Function 4  <— group 0
Function 2  <— group 1
Function 1  <— back

他的執行順序,基本上就是先執行 at_front 的 slotFunc5(),再去執行群組裡的 slot,也就是 group 0 的slotFunc3()slotFunc4(),以及 group 1 的slotFunc2(),等這些都執行完後,最後執行at_back 的slotFunc1()

而實際上,如果有需要的話,每一個 group 也都一樣可以指定 at_front 和 at_back 這兩種連結位置,在群組裡再做排序的控制,不過這邊就不舉例了。

最後補充說明一下,實際上 Signals2 的 slot group 型別,雖然預設是使用 int,但是實際上也是 template、可以自己去定義的~有興趣的話,可以去參考「boost/signals2/preprocessed_signal.hpp」這個檔案(連結)裡面針對boost::signals2::signal 這個 template class 的定義,應該就知道該怎麼指定群組的型別了~

 

中斷連線

雖然一般的狀況下,在連接 signal 和 slot 後,就不會再去動這個連結關係了,但是某些情況下,可能還是會需要解除連結關係。這時候,Boost::Signals2 提供了幾種作法,可以中斷 singal 和 slot 之間的關係。

  • 使用 signal::disconnect_all_slots() 中斷所有連結

    這是一個最簡單的方法,只要執行了 signal 的 disconnect_all_slots() 這個成員函式,他就會把所有已經建立的連結都中斷掉。例如在上面的例子裡,如果加入一行「mSignal1.disconnect_all_slots();」,那之後再去執行mSignal1() 時,就不會再呼叫到任何 slot function 了~

  • 使用 signal::disconnect() 中斷指定的連結

    上面的方法是中斷所有的連結,而如果只是要中斷特定的連結的話,也可以透過 disconect() 這個函式來做到。他有兩個版本,第一個版本是指定要中斷和哪一個 slot 的連結、第二個則是指定要中斷哪一個 slot group 的連結。

    以上面的範例程式來說,只要執行了「mSignal1.disconnect( slotFunc1 )」,就可以中斷mSignal1 這個 signal 和 slotFunc1() 之間的連結;而如果執行「mSignal1.disconnect( 0 )」的話,則可以中斷mSignal1 group 0 這個 slot group 裡的所有 slot 的連結,包括了slotFunc3()slotFunc4()

    透過這兩種使用方法,基本上應該就可以應付大部分要切斷連結的狀況了~不過要注意的是,其實 Signals2 的 signal 是允許同一個 signal 重複連結到同一個 slot 的;而在重複連結的情況下,同一個 slot function 也會被重複呼叫。相對於此,disconnect() 則是會將所有符合條件的連結都中斷掉,這是在使用上可能要注意的。下面是一個簡單的例子:

    [cpp]  view plain copy
    1. // 1. connect signal and slot  
    2. mSignal1.connect( slotFunc1 );  
    3. mSignal1.connect( slotFunc1 );  
    4. mSignal1.connect( slotFunc2 );  
    5.   
    6. // 2. emit the signal  
    7. std::cout << "Emit signal" << std::endl;  
    8. mSignal1();  
    9.   
    10. // 3. disconnect slotFunc1  
    11. mSignal1.disconnect( slotFunc1 );  
    12.   
    13. // 4. emit the signal  
    14. std::cout << "Emit signal after disconnect slotFunc1()" << std::endl;  
    15. mSignal1();  


    這個程式碼的執行結果,會是下面這樣子:

    Emit signal
  • Function 1
  • Function 1
  • Function 2
  • Emit signal after disconnect slotFunc1()
  • Function 2

    可以發現,在第一次 emit 這個 signal 的時候,slotFunc1() 由於被連結了兩次,所以也被執行了兩次;但是在mSignal1.disconnect( slotFunc1 ) 過後,再去 emit 這個 signal 的話,由於slotFunc1() 的兩個連結都被中斷了,所以就剩下 slotFunc2() 會被執行到了。

  • 透過 boost::signals2::connection 中斷指定的連結

    除了透過 signal 的成員函式來進行中斷連線之外,其實也可以透過 Boost::Signals2 提供了boost::signals2::connection 型別的物件,來做個別連線的管理;它的使用方法大致如下:

    [cpp]  view plain copy
    1. // 1. connect signal and slot  
    2. boost::signals2::connection c1, c2, c3;  
    3. c1 = mSignal1.connect( slotFunc1 );  
    4. c2 = mSignal1.connect( slotFunc1 );  
    5. c3 = mSignal1.connect( slotFunc2 );  
    6.   
    7. // 2. emit the signal  
    8. std::cout << "Emit signal" << std::endl;  
    9. mSignal1();  
    10.   
    11. // 3. disconnect the first connection  
    12. c1.disconnect();  
    13.   
    14. // 4. emit the signal  
    15. std::cout << "Emit signal after disconnect 1st connection" << std::endl;  
    16. mSignal1();  


    在上面的程式碼裡可以看到,實際上在透過 signal::connect() 建立 signal 和 slot 間的連結的時候,都會傳回一個型別是 boost::signals2::connection 的物件,而這個物件就是用來做 signal 和  slot 之間,個別連結的管理的;如果要切斷個別的連結的話,就只要呼叫他的disconnect() 函式就可以了~

    像以上面的程式碼在執行後,結果就會是:

    Emit signal
  • Function 1
  • Function 1
  • Function 2
  • Emit signal after disconnect 1st connection
  • Function 1
  • Function 2

    可以發現,透過這樣來中斷連結的話,就不會像使用 signal::connect() 一樣,把重複的連結也都中斷,而可以只中斷自己想要的連結了~不過相對的,要用這東西的話,就還得自己額外去管理這些connection 的物件,會比較麻煩就是了。

暫時停止連線(block)

上面一個段落,主要是在講要如何中斷一個連線;但是其實在某些時候,我們需要的不是永遠中斷這些連線,而只是需要暫時停止這些連線(一般是稱為「block」,通常是為了怕 signal / slot 造成無窮迴圈),這時候該怎麼辦呢?Signals2 為了這種狀況,提供了boost::signals2::shared_connection_block,讓程式設計師可以來做連線狀態的管理;它的使用方法大致會像下面這個樣子:

[cpp]  view plain copy
  1. // include STL headers  
  2. #include <stdlib.h>  
  3. #include <iostream>  
  4.   
  5. // include Boost header  
  6. #include <boost/signals2/signal.hpp>  
  7. #include <boost/signals2/shared_connection_block.hpp>  
  8.   
  9. // slot function  
  10. void slotFunc1(){ std::cout << "Function 1" << std::endl; }  
  11. void slotFunc2(){ std::cout << "Function 2" << std::endl; }  
  12.   
  13. int main( int argc, char** argv )  
  14. {  
  15.   // create signal  
  16.   boost::signals2::signal<void ()> mSignal;  
  17.   
  18.   // connect signal and slot  
  19.   boost::signals2::connection c1 = mSignal.connect( slotFunc1 ),  
  20.                               c2 = mSignal.connect( slotFunc2 );  
  21.  {  
  22.     // block the connection in this scope  
  23.     boost::signals2::shared_connection_block block( c1 );  
  24.     // emit the signal  
  25.     std::cout << "C1 is blocked" << std::endl;  
  26.     mSignal();  
  27.   }  
  28.   
  29.   // emit the signal  
  30.   std::cout << "unblock scope" << std::endl;  
  31.   mSignal();  
  32.   
  33.   return 0;  
  34. }  


上面的執行結果,會是:

C1 is blocked
Function 2
unblock scope
Function 1
Function 2

首先,和 signals2 裡面其他型別不太一樣,要使用 shared_connection_block 這個型別必須要額外 include「boost/signals2/shared_connection_block.hpp」這個檔案才可以使用。而在使用時,他必須要有本來連結的connection,才能建立、操作;所以在這裡,用c1 和 c2 來個別紀錄 mSignal 和 slotFunc1()以及 slotFunc2() 的連線。

而在上面程式碼猶大括號({ })圈起來的黃底區域(scope、生存空間)裡面,就是建立了一個型別是boost::signals2::shared_connection_block 的物件 block,並且將要 block 的連線 c1 傳入,讓他知道要管理的連線。而在這樣建立 block 這個物件後,c1 這個連線就已經被 block 掉了~所以在之後 emitmSignal 的時候,只會執行到 slotFunc2() 這個函式、而不會執行 slotFunc1() 的內容。

不過當出了黃色區域這個 scope 後,block 這個物件就會消失了,而在他消失前,會把它 block 掉的連線還原,所以之後再去 emitmSignal 的時候,就會連 slotFunc1() 一起執行了。

雖然這邊只是單純透過 scope 來 block signal 和 slot 連線,不過實際上,shared_connection_block 的物件也具有 block() 和 unblock() 這兩個成員函式,可以用來控制是否要 block 掉 signal 和 slot 間的連線。所以有需要的話,也是可以自己去透過管理一份shared_connection_block 的物件,來做 block 的控制。

 

話說,本來只打算分上下兩篇的,不過後來看來要把剩下想寫的東西都塞到這篇好像會變得太長,所以還是決定再拆一篇出來,專門來講自動連線管理好了。

 

Boost 的事件管理架構:Signal / Slot(下)

關於 Boost 的 signals2 這個函式庫,在第一篇的時候是在針對他做說明,以及列了一些最簡單的使用狀況;而在第二篇,則是針對 slot 的順序控制、連線的管理,做一些進一步的說明。

而這一篇呢,則是在最後,針對 signal /slot 在物件上的操作,以及自動連接管理,做一些說明。


Scoped Connection

首先,Boost 在 Signals2 裡,有提供一個 boost::signals2::scoped_connection 的類別,可以透過這個型別的物件存在的與否,來做 signal / slot 連線的控制;它的基本使用方法大致如下:

[cpp]  view plain copy
  1. // include STL headers  
  2. #include <stdlib.h>  
  3. #include <iostream>  
  4.   
  5. // include Boost header  
  6. #include <boost/signals2/signal.hpp>  
  7.   
  8. // slot function  
  9. void slotFunc1(){ std::cout << "Function 1" << std::endl; }  
  10. void slotFunc2(){ std::cout << "Function 2" << std::endl; }  
  11.   
  12. int main( int argc, char** argv )  
  13. {  
  14.   // create a signal  
  15.   boost::signals2::signal<void () > mSignal1;  
  16.   {  
  17.     boost::signals2::scoped_connection sc( mSignal1.connect( slotFunc1 ) );  
  18.     mSignal1.connect( slotFunc2 );  
  19.   
  20.     // emit the signal  
  21.     std::cout << "EMIT first time" << std::endl;  
  22.     mSignal1();  
  23.   }  
  24.   
  25.   // emit the signal  
  26.   std::cout << "EMIT second time" << std::endl;  
  27.   mSignal1();  
  28.   
  29.   return 0;  
  30. }  


在上面的例子裡,mSignal1 在黃底的這個 scope 中,連接了slotFunc1() 和 slotFunc2() 這兩個 slot;比較特別的是,在連接 slotFunc1() 時,又將所傳回的 connection 交給了型別是 scoped_connection的物件 sc 來做管理(他的使用方法基本上和之前介紹過的 shared_connection_block 類似)。在經過這樣的設定之後,mSignal1 和slotFunc1() 之間的連結,就會變成是由 sc 這個物件來做控制。

scoped_connection 這個類別是繼承自本來的connection,所以一樣可以透過sc 的 disconnect() 函式來中斷連線;但是不同的是,scoped_connection 所代表的連線,會在他的物件消失時,自動中斷連線。

所以上面的程式,在黃底的 scope 裡執行 mSignal1() 的時候,因為 sc 還存在,所以 slotFunc1() 和slotFunc2() 這兩個 slot 都會被執行到。但是等到出了黃底的 scope 後,由於物件 sc 已經消失了,所以mSignal1 和 slotFunc1() 之間的連結也就跟著中斷了;也因此,之後再執行 mSignal1(),就只會執行到slotFunc2() 了。而實際上,上面的程式執行結果會是:

EMIT first time
Function 1
Function 2
EMIT second time
Function 2

這樣一來,透過 scoped_connection 物件的存在與否,來控制 signal / slot 連線的狀態了~而最簡單的應用,就如同它的名稱,變成是被 scope 限制住的連結了。

而某種程度上,如果可以進一步自己去控制這個物件的存在與否,那也算是可以拿來做連線管理的方法之一。不過實際上,scoped_connection 本來的設計並不是用來拿來做自動連線管理的,所以在操作上會比較麻煩,還要額外去做管理scoped_connection 的物件;而且由於這個型別是 non-copyable、不可複製的,所以在使用上其實會有不少限制。

 

使用類別的成員函式當作 slot

雖然 scoped_connection 在某種程度上可能可以做到自動連線管理,但是實際上,Signals2 是有專門的方法,可以用來自動根據物件的存在,來管理 signal / slot 的連結的。不過在講自動連線之前,這邊得先大概提一下,怎麼樣去把一個物件的成員函式(member function)當作是 slot function。

要做到這件事,最直接通用的方法,就是直接透過 TR1 的 bind()(註一),把物件的成員函式封包成一個 funciton object,再傳給 signal::connect();關於 bind() 這部分,由於不是這裡的主題,所以相關的說明就請參考之前的《在 C++ 裡傳遞、儲存函式 Part 3:Function Object in TR1》一文。

而下面則是一個在 signal / slot 裡使用的 bind() 簡單範例:

[cpp]  view plain copy
  1. // include STL headers  
  2. #include <stdlib.h>  
  3. #include <iostream>  
  4. #include <complex>  
  5.   
  6. // include Boost header  
  7. #include <boost/signals2/signal.hpp>  
  8.   
  9. // the class with slot function  
  10. class CObject  
  11. {  
  12. public:  
  13.   int  m_ObjIndex;  
  14.   
  15.   CObject( int idx )  
  16.   {  
  17.     m_ObjIndex  = idx;  
  18.   }  
  19.   
  20.   void slotFunc()  
  21.   {  
  22.     std::cout << "Object " << m_ObjIndex << std::endl;  
  23.   }  
  24. };  
  25.   
  26. int main( int argc, char** argv )  
  27. {  
  28.   // create signal  
  29.   typedef boost::signals2::signal<void ()> TSignalType;  
  30.   TSignalType  mSignal;  
  31.   
  32.   // create object  
  33.   CObject  *pObj1 = new CObject( 1 );  
  34.   
  35.   // connect signal /slot  
  36.   mSignal.connect( std::bind( &CObject::slotFunc, pObj1 ) );  
  37.   
  38.   // emit signal  
  39.   mSignal();  
  40.   
  41.   return 0;  
  42. }  



在上面的程式碼裡,首先是定義了一個名為 CObject 的類別,裡面只有一個紀錄自己 index 的變數、建構子、以及當作 slot 的成員函式 slotFunc()

在主程式裡面,一樣是先建立出 signal 的物件,不過在這邊是先透過 typedef 將 signal 的型別定義為TSignalType,可以用來簡化之後的程式碼。接著,則是產生一個CObject 的物件 pObj1,並透過signal::connect() 來將他的的成員函式 slotFunc() 和 mSignal 做連結。而在使用上,就是透過 std::bind() 來將他作封包了~實際的程式寫法,就是上方黃底的部分;而如果 signal / slot 是有額外的參數的話,還需要再加上 placeholder,不過這算是std::bind() 的細節,所以在這邊就不多提了。

而除了使用 TR1 的 bind() 可以將物件的成員函式封包成 function object 外,其實 Signals2 也有提供另外的方案,可以把物件的成員函式,轉換為對應的 slot function 型別。他的寫法就是:

TSignalType::slot_type( &CObject::slotFunc, pObj1 )

slot_type 實際上就是 Signals2 內部用來傳遞、紀錄對應 signal 的 slot function 的型別;signal::connect() 所需要傳進的 slot function,其實也就是這個型別。在一般的使用狀況下,connect() 的時候會將 funciton object 自動轉換成 slot_type;而這邊所使用的,則是他額外的建構方法,手動將物件的成員函式,建構成slot_type 的物件。

如此一來,signal 和 slot 連結的程式,就會變成:

mSignal.connect( TSignalType::slot_type( &CObject::slotFunc, pObj1 ) );

而這樣的寫法,和上面使用 std::bind() 的寫法,結果基本上會是相同的。而實際上,他在介面和用法上是和 TR1 的 bind() 也是相同的(實際上他的內部應該就是去呼叫 bind()),在這邊也就不贅述了。

 

自動連線管理

當使用一個物件的成員函式當作 slot 的時候,最大的問題會在於,就算這個物件消失了,signal 被觸發的時候,還是會試圖去執行這個已經已經消失的物件的成員函式,而導致程式出問題。例如以上面的例子來說,如果在 emit signal 前,把pObj1 這個物件刪除的話,那執行「mSignal();」的結果就會有問題;像下面的程式碼,就是一個會出問題的程式:

// connect signal /slot mSignal.connect( TSignalType::slot_type( &CObject::slotFunc, pObj1 ) ); // emit signal delete pObj1; mSignal();

而要怎樣避免這個問題呢?Signals2 在 slot 這邊提供了 track() 的功能,讓他可以搭配 Boost 的shared_ptr參考文件;註二)去追蹤指定物件的存在狀況,並藉此來確認是控制 signal / slot 之間的連結。下面就是一個根據上面的程式所修改出來的簡單例子:

[cpp]  view plain copy
  1. // create signal  
  2. typedef boost::signals2::signal<void ()> TSignalType;  
  3. TSignalType  mSignal;  
  4.   
  5. // create object  
  6. CObject  *pObj1 = new CObject( 1 );  
  7.   
  8. // connect signal /slot  
  9. {  
  10.   boost::shared_ptr<CObject> spObj( pObj1 );  
  11.   mSignal.connect( TSignalType::slot_type( &CObject::slotFunc, spObj.get() ).track( spObj ) );  
  12.   
  13.   // emit signal  
  14.   mSignal();  
  15. }  
  16.   
  17. // emit signal  
  18. mSignal();  


在這個程式裡,進入黃底的 scope 後,CObject 的物件pObj1 會改成使用 spObj 這個boost::shared_ptr 型別的物件來做管理;shared_ptr 在使用上會很類似標準的指標,不過他會記錄有pObj1 被多少個 shared_ptr 使用 ,如果都沒有的話,就會自動把 pObj1 給刪除掉、避免 memory leak。由於這邊只有 spObj 一個實體有使用到 pObj1,所以在離開他所屬的 scope 後,自己要消失的時候,就會把 pObj1 的資料也給刪除掉,相當於執行了 delete pObj1

而為了避免 pObj1 被刪除後,mSingal 還是會去執行他的slotFunc(),所以這邊在建立slot_type 的物件的時候,還另外透過 slot_type 的 track() 這個函式,讓他去追蹤 spObj 這個物件。如此一來,在離開黃底的 scope 後,spObj 本身消失連帶刪除pObj1 資料的同時,也會自動切斷 mSignal 和 pObj1->slotFunc() 之間的連結。

也因此,上面的程式碼在第一次執行「mSignal();」時(黃底的 scope 內),會呼叫到pObj1->slotFunc();但是第二次執行「mSignal();」時(黃底的 scope 外),則由於mSignal 和pObj1->slotFunc() 之間的連結已經被自動切斷了,所以也就不會執行到 pObj1->slotFunc() 了~

如此一來,就可以做到根據物件的生命週期,自動決定 signal / slot 連線與否的功能了;而這樣,也就可以避免試圖去呼叫已經刪除的物件的函示了。不過相對的,這樣的缺點,就是要拿來用物件,勢必得被boost::shared_ptr 這種自動資源管理的物件綁死了…所以到底要不要這樣用,可能就是要自己取捨了。

另外,track() 實際上是把要追蹤的物件,以清單的形式儲存下來,所以如果有必要的話,也可以透過重複呼叫track(),來同時追蹤多個物件,而其中只要有一個物件消失了,連線就會中斷。而此外,其實track() 也可以用來追蹤別的 signal 和別的 slot(註三),不過 Heresy 個人是覺得意義不是很大,所以在這邊就不額外提了,有興趣的話可以參考官方文件(網頁),裡面有進一步的說明。

 

對於 Boost Signals2 這個函式庫的介紹,大概就先寫到這了。實際上,他還有一些額外的進階用法(尤其是 thread 相關的),不過在這邊就先略過不提了,有需要的人,就麻煩自己去看官方文件吧~

附註:
  1. 如果編譯器不支援 TR1 的話,也可以使用 Boost 的 bind;而實際上官方範例是使用 Boost 本身的 bind。
  2. track() 實際上使用的是由shared_ptr 取得的 weak_ptr,比避免造成 shared_ptr 內的計數器把這部分也算進去。另外,雖然 TR1 裡也已經有 shared_ptr 了,但是由於無法和 Boost 的版本做型別轉換的關係,所以在這裡只能用 Boost 的版本。
  3. 在透過 track() 追蹤 slot 的時候,實際上是去追蹤「被追蹤的 slot 所追蹤的物件」,而非所指定的 slot 本身被追蹤。也就是當執行 slot1.track( slot2 ); 的時候,slot1 會額外去追蹤slot2 有在追蹤的物件,但是不會去追蹤 slot2 本
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值