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

| | 0 Comments| 08:59
Categories:

隔了很久了,不過這篇也是之前 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( &quotient );
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;在上面的例子裡,就是 firstlast;這樣的設計結果,是為了讓他的操作和使用一般的 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( &quotient );
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() 的回傳值型別不會像之前一樣是 boost::optional<float>,而會直接是 result_type 所代表的 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 的連結管理,做大概的介紹。

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *