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

本文介绍了Boost库中的Signals2组件,这是一个线程安全的事件管理库。通过信号(signal)和槽(slot)机制实现事件的发布和订阅,支持运行时动态连接。文章通过示例演示了基本使用方法,包括如何定义信号、连接槽函数、发射信号以及处理返回值。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

原帖: http://viml.nchc.org.tw/blog/paper_info.php?CLASS_ID=1&SUB_ID=1&PAPER_ID=226


隔了很久了,不過這篇也是之前 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;在上面的例子裡,就是 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( &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 的連結管理,做大概的介紹。



在编写客户端TCP通信程序时,选择合适的工具库非常重要。Qt的`QTcpSocket`和Boost.Asio都是非常优秀的跨平台网络编程库,它们各有特点和适用场景。 ### 使用 Qt 的 QTcpSocket 开发 TCP 客户端的优势 1. **集成度高**:如果您的项目已经基于 Qt 框架构建,则可以直接利用 `QTcpSocket` 来简化网络功能开发。它与其他模块如 GUI、事件循环等高度整合。 2. **易于使用**:API 设计直观易懂,适合快速上手并完成基本到中级复杂程度的任务。 3. **信号槽机制支持异步操作**:通过内置的支持非阻塞 I/O 和线程安全的设计模式(例如信号与槽),可以方便地处理数据接收及发送而不需额外管理多线程问题。 4. **文档丰富且社区活跃**:由于广泛的应用范围,在遇到困难时容易找到解决方案。 示例代码片段: ```cpp #include <QTcpSocket> // 创建实例化对象 QTcpSocket *socket = new QTcpSocket(this); connect(socket,SIGNAL(readyRead()),this,SLOT(readData())); ``` ### 使用 Boost::Asio 进行 TCP 客户端开发的优点 1. **高性能潜力**:对于需要极致性能的应用来说,Boost.Asio 提供了更底层控制能力以及优化空间。 2. **灵活性强**:允许开发者自行定制更多细节部分比如定时器设置、自定义缓冲区大小等等;而且不仅仅局限于套接字通讯还涵盖了其他多种IO模型。 3. **便携性强**:作为一个独立组件能够轻松嵌入任何 C++ 程序当中而无需依赖整个图形界面系统或其他庞大的基础架构。 4. **丰富的特性集合**:除了传统的同步/异步读写之外还包括SSL加密传输等功能选项。 简单例子展示如何创建连接: ```cpp using boost::asio; ip::tcp::iostream stream("www.example.com", "http"); if (!stream) { std::cerr << "Connect failed!"<<std::endl; } else{ // 成功建立链接后的逻辑... } ``` ### 我的建议 如果您正在做一个包含用户交互或者多媒体播放之类的桌面应用程序,并且希望减少学习成本同时提高生产力的话那么选用 Qt 可能更为合适因为其提供了统一风格并且成熟的框架体系结构可以帮助我们更快更好地达成目标; 相反地当我们面对的是一个后台服务进程或者是对实时性和吞吐量有着极高要求的情况之下就应该考虑采用 Boost.Asio 因为其提供的强大功能足以满足那些苛刻条件下高效稳定运行的需求. 最终的选择还是要依据实际项目的具体情况来做权衡考量!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值