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

 

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() 的回傳值型別不會像之前一樣是 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 的連結管理,做大概的介紹。

 

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 外,再額外加上指定的連結位置參數就可以了~下面就是一個簡單的例子:

// 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 類似)。在經過這樣的設定之後,mSignal1slotFunc1() 之間的連結,就會變成是由 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 外),則由於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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值