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 類似)。在經過這樣的設定之後,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 本身。

Boost 的事件管理架構:Signal / Slot(上)
Boost 的事件管理架構:Signal / Slot(中)
Boost 的事件管理架構:Signal / Slot(下)
Boost C++ Libraries 目錄

廣告

關於 Heresy
https://kheresy.wordpress.com

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

  1. 引用通告: Boost 與 Qt 的 Signal / Slot 效能測試 | Heresy's Space

  2. 引用通告: Boost 的事件管理架構:Signal / Slot(中) « Heresy's Space

  3. 引用通告: Boost 的事件管理架構:Signal / Slot(上) « Heresy's Space

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com Logo

您的留言將使用 WordPress.com 帳號。 登出 / 變更 )

Twitter picture

您的留言將使用 Twitter 帳號。 登出 / 變更 )

Facebook照片

您的留言將使用 Facebook 帳號。 登出 / 變更 )

Google+ photo

您的留言將使用 Google+ 帳號。 登出 / 變更 )

連結到 %s

%d 位部落客按了讚: