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 自己覺得應該比較用的到的部分,做一些簡單的說明。
首先,先來個最簡單的範例程式:
// include STL headers
#include <stdlib.h>
#include <iostream>
// include Boost header
#include <boost/signals2/signal.hpp>
// slot function
void slotTest1( int a )
{
std::cout << "Test1 get " << a << std::endl;
}
void slotTest2( int a )
{
std::cout << "Test2 get " << a << std::endl;
}
int main( int argc, char** argv )
{
// create a signal
boost::signals2::signal<void (int)> mSignal1;
// connect signal and slot
mSignal1.connect( slotTest1 );
mSignal1.connect( slotTest2 );
// emit signal
mSignal1( 10 );
return 0;
}
首先,要使用 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 的類別如下:
class CTestSlot
{
public:
void operator() ( int x )
{
std::cout << "CTestSlot get " << x << std::endl;
}
};
然後再透過 mSignal1.connect( CTestSlot() ); 來做連接,這樣也是可以的。
預設的 signal 回傳值
在上面的例子裡面,slot 是沒有回傳值的,那如果遇到有需要回傳值的時候,要怎麼處理呢?在預設、沒有特別處理的狀況下,signal 會將最後一個 slot 函式所回傳的值,當作整個 signal 執行後的回傳值傳回來;像以官方的範例來說,他的程式碼是:
// include STL headers
#include <stdlib.h>
#include <iostream>
// include Boost header
#include <boost/signals2/signal.hpp>
// slot function
float product(float x, float y) { return x * y; }
float quotient(float x, float y) { return x / y; }
float sum(float x, float y) { return x + y; }
float difference(float x, float y) { return x - y; }
int main( int argc, char** argv )
{
// create a signal
boost::signals2::signal<float (float, float)> mSignal1;
// connect signal and slot
mSignal1.connect( &product );
mSignal1.connect( "ient );
mSignal1.connect( &sum );
mSignal1.connect( &difference );
// emit signal
std::cout << *mSignal1( 10, 3 ) << std::endl;
return 0;
}
而最後輸出的結果,則會是最後一個執行到的函式 difference() 的結果,也就是「7」。不過要注意的是,這邊的回傳值的型別是經過封包過的,他的型別並不是 float,而是
,要取得他真正的值,要在他前面再加個「*」。boost::optional<float>
自訂回傳值處理方法
那如果有需要其他的 slot 回傳的值該怎麼辦呢?Boost 的 Signals2 在這時候,有提供所謂的「combiner」可以用來處理所有 slot 的回傳值。例如在官方的範例裡,就是定義了一個名為maximum 的 struct 來作取最大值的動作,其內容如下(略作簡化):
struct maximum
{
typedef float result_type;
template<typename InputIterator>
float operator()(InputIterator first, InputIterator last) const
{
// If there are no slots to call,
// just return the default-constructed value
if(first == last )
return 0.0f;
float max_value = *first++;
while( first != last )
{
if (max_value < *first)
max_value = *first;
++first;
}
return max_value;
}
};
在這個 struct 裡,很重要的一點是,為了要讓 signal 知道他回傳值的型別,所以必須要去定義代表回傳執型別的「result_type」,這點和在使用 TR1 的 bind() 時是類似的(參考)。
而再來就是要去定義他的 operator(),它的形式基本上會是 template 的,必須要有兩個參數,一個代表起始值的 iterator、第二個則是結束值的 iterator;在上面的例子裡,就是first 和 last;這樣的設計結果,是為了讓他的操作和使用一般的 STL container 的 iterator 時是一致的。而 operator() 裡,就是針對給定的 iterator 範圍裡的值,去找到最大值了~
要怎麼用這個 maximum 呢?Combiner 對 signal 來說,也是透過 template 來控制的,所以要指定 combiner,就是在建立 signal 就要指定的~它的使用方法如下:
// create a signal
boost::signals2::signal<float (float, float), maximum > mSignal1;
// connect signal and slot
mSignal1.connect( &product );
mSignal1.connect( "ient );
mSignal1.connect( &sum );
mSignal1.connect( &difference );
// emit signal
std::cout << mSignal1( 10, 3 ) << std::endl;
如此一來,在 emit mSignal1 這個 signal 的時候,電腦就會透過 maximum,來取所連接的四個 slot 所回傳計算出來的結果的最大值了;在這個例子來說,結果會是 product() 的 10 * 3,也就是「30」。
另外要注意的是,由於在給定的 combiner maximum 裡已經有定義了 result_type,所以執行 signal 的 operator() 的回傳值型別不會像之前一樣是
,而會直接是result_type 所代表的 float;所以在讀取他的值的時候,也不需要再加上「*」了。boost::optional<float>
而實際要使用的時候,combiner 的 result_type 也不見得要和 slot 的回傳值型別一樣,也可以定義成其他的型別;像官方範例裡面,就以 aggregate_values 做範例,將所有 slot 回傳的值,都儲存到一個 vector 裡記錄下來。其程式碼如下(略作簡化):
struct aggregate_values
{
typedef std::vector<float> result_type;
template<typename InputIterator>
result_type operator()(InputIterator first, InputIterator last) const
{
result_type values;
while(first != last)
{
values.push_back(*first);
++first;
}
return values;
}
};
在這個例子裡,result_type 是被定義為std::vector<float>;而operator() 裡所做的事,就是單純地把所有資料都往這個 vector 裡面塞了。
而在使用時,基本上就和使用 maximum 這個 combiner 時一樣,把 mSignal1 的型別改為「boost::signals2::signal<float (float, float), 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 的時候,實際上是分三個階段來執行的:
- 使用 boost::signals2::at_front 連結的 slot
- 指定群組(group)的 slot
- 使用 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 外,再額外加上指定的連結位置參數就可以了~下面就是一個簡單的例子:
// include STL headers
#include <stdlib.h>
#include <iostream>
// include Boost header
#include <boost/signals2/signal.hpp>
// slot function
void slotFunc1(){ std::cout << "Function 1" << std::endl; }
void slotFunc2(){ std::cout << "Function 2" << std::endl; }
void slotFunc3(){ std::cout << "Function 3" << std::endl; }
void slotFunc4(){ std::cout << "Function 4" << std::endl; }
void slotFunc5(){ std::cout << "Function 5" << std::endl; }
int main( int argc, char** argv )
{
// create a signal
boost::signals2::signal<void () > mSignal1;
// connect signal and slot
mSignal1.connect( slotFunc1 );
mSignal1.connect( slotFunc2, boost::signals2::at_back );
mSignal1.connect( slotFunc3, boost::signals2::at_front );
mSignal1.connect( slotFunc4 );
mSignal1.connect( slotFunc5, boost::signals2::at_front );
// emit the signal
mSignal1();
return 0;
}
在這個範例裡,定義了五個 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() 則是會將所有符合條件的連結都中斷掉,這是在使用上可能要注意的。下面是一個簡單的例子:
// 1. connect signal and slot mSignal1.connect( slotFunc1 ); mSignal1.connect( slotFunc1 ); mSignal1.connect( slotFunc2 ); // 2. emit the signal std::cout << "Emit signal" << std::endl; mSignal1(); // 3. disconnect slotFunc1 mSignal1.disconnect( slotFunc1 ); // 4. emit the signal std::cout << "Emit signal after disconnect slotFunc1()" << std::endl; 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 型別的物件,來做個別連線的管理;它的使用方法大致如下:
// 1. connect signal and slot boost::signals2::connection c1, c2, c3; c1 = mSignal1.connect( slotFunc1 ); c2 = mSignal1.connect( slotFunc1 ); c3 = mSignal1.connect( slotFunc2 ); // 2. emit the signal std::cout << "Emit signal" << std::endl; mSignal1(); // 3. disconnect the first connection c1.disconnect(); // 4. emit the signal std::cout << "Emit signal after disconnect 1st connection" << std::endl; 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,讓程式設計師可以來做連線狀態的管理;它的使用方法大致會像下面這個樣子:
// include STL headers
#include <stdlib.h>
#include <iostream>
// include Boost header
#include <boost/signals2/signal.hpp>
#include <boost/signals2/shared_connection_block.hpp>
// slot function
void slotFunc1(){ std::cout << "Function 1" << std::endl; }
void slotFunc2(){ std::cout << "Function 2" << std::endl; }
int main( int argc, char** argv )
{
// create signal
boost::signals2::signal<void ()> mSignal;
// connect signal and slot
boost::signals2::connection c1 = mSignal.connect( slotFunc1 ),
c2 = mSignal.connect( slotFunc2 );
{
// block the connection in this scope
boost::signals2::shared_connection_block block( c1 );
// emit the signal
std::cout << "C1 is blocked" << std::endl;
mSignal();
}
// emit the signal
std::cout << "unblock scope" << std::endl;
mSignal();
return 0;
}
上面的執行結果,會是:
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 連線的控制;它的基本使用方法大致如下:
// include STL headers
#include <stdlib.h>
#include <iostream>
// include Boost header
#include <boost/signals2/signal.hpp>
// slot function
void slotFunc1(){ std::cout << "Function 1" << std::endl; }
void slotFunc2(){ std::cout << "Function 2" << std::endl; }
int main( int argc, char** argv )
{
// create a signal
boost::signals2::signal<void () > mSignal1;
{
boost::signals2::scoped_connection sc( mSignal1.connect( slotFunc1 ) );
mSignal1.connect( slotFunc2 );
// emit the signal
std::cout << "EMIT first time" << std::endl;
mSignal1();
}
// emit the signal
std::cout << "EMIT second time" << std::endl;
mSignal1();
return 0;
}
在上面的例子裡,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() 簡單範例:
// include STL headers
#include <stdlib.h>
#include <iostream>
#include <complex>
// include Boost header
#include <boost/signals2/signal.hpp>
// the class with slot function
class CObject
{
public:
int m_ObjIndex;
CObject( int idx )
{
m_ObjIndex = idx;
}
void slotFunc()
{
std::cout << "Object " << m_ObjIndex << std::endl;
}
};
int main( int argc, char** argv )
{
// create signal
typedef boost::signals2::signal<void ()> TSignalType;
TSignalType mSignal;
// create object
CObject *pObj1 = new CObject( 1 );
// connect signal /slot
mSignal.connect( std::bind( &CObject::slotFunc, pObj1 ) );
// emit signal
mSignal();
return 0;
}
在上面的程式碼裡,首先是定義了一個名為 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 之間的連結。下面就是一個根據上面的程式所修改出來的簡單例子:
// create signal
typedef boost::signals2::signal<void ()> TSignalType;
TSignalType mSignal;
// create object
CObject *pObj1 = new CObject( 1 );
// connect signal /slot
{
boost::shared_ptr<CObject> spObj( pObj1 );
mSignal.connect( TSignalType::slot_type( &CObject::slotFunc, spObj.get() ).track( spObj ) );
// emit signal
mSignal();
}
// emit signal
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 相關的),不過在這邊就先略過不提了,有需要的人,就麻煩自己去看官方文件吧~
附註:
- 如果編譯器不支援 TR1 的話,也可以使用 Boost 的 bind;而實際上官方範例是使用 Boost 本身的 bind。
- track() 實際上使用的是由shared_ptr 取得的 weak_ptr,比避免造成 shared_ptr 內的計數器把這部分也算進去。另外,雖然 TR1 裡也已經有 shared_ptr 了,但是由於無法和 Boost 的版本做型別轉換的關係,所以在這裡只能用 Boost 的版本。
- 在透過 track() 追蹤 slot 的時候,實際上是去追蹤「被追蹤的 slot 所追蹤的物件」,而非所指定的 slot 本身被追蹤。也就是當執行 slot1.track( slot2 ); 的時候,slot1 會額外去追蹤slot2 有在追蹤的物件,但是不會去追蹤 slot2 本