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

| | 2 Comments| 13:57
Categories:

關於 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 類似)。在經過這樣的設定之後,mSignal1slotFunc1() 之間的連結,就會變成是由 sc 這個物件來做控制。

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

所以上面的程式,在黃底的 scope 裡執行 mSignal1() 的時候,因為 sc 還存在,所以 slotFunc1()slotFunc2() 這兩個 slot 都會被執行到。但是等到出了黃底的 scope 後,由於物件 sc 已經消失了,所以 mSignal1slotFunc1() 之間的連結也就跟著中斷了;也因此,之後再執行 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_typetrack() 這個函式,讓他去追蹤 spObj 這個物件。如此一來,在離開黃底的 scope 後,spObj 本身消失連帶刪除 pObj1 資料的同時,也會自動切斷 mSignalpObj1->slotFunc() 之間的連結。

也因此,上面的程式碼在第一次執行「mSignal();」時(黃底的 scope 內),會呼叫到 pObj1->slotFunc();但是第二次執行「mSignal();」時(黃底的 scope 外),則由於 mSignalpObj1->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 本身。

2 thoughts on “Boost 的事件管理架構:Signal / Slot(下)”

發佈回覆給「旭东」的留言 取消回覆

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