天天看點

boost C++知識點(三)

4 事件處理

4.1 信号signals:

雖然這個庫的名字乍一看好象有點誤導,但實際上并非如此。 Boost.Signals 所實作的模式被命名為 ‘信号至插槽’ (signal to slot),它基于以下概念:當對應的信号被發出時,相關聯的插槽即被執行。 原則上,你可以把單詞 ‘信号’ 和 ‘插槽’ 分别替換為 ‘事件’ 和 ‘事件處理器’。 不過,由于信号可以在任意給定的時間發出,是以這一概念放棄了 ‘事件’ 的名字。

是以,Boost.Signals 沒有提供任何類似于 ‘事件’ 的類。 相反,它提供了一個名為 boost::signal 的類,定義于 boost/signal.hpp. 實際上,這個頭檔案是唯一一個需要知道的,因為它會自動包含其它相關的頭檔案。

Boost.Signals 定義了其它一些類,位于 boost::signals 名字空間中。 由于 boost::signal 是最常被用到的類,是以它是位于名字空間 boost 中的。

#include <boost/signal.hpp> 
#include <iostream> 

void func() 
{ 
  std::cout << "Hello, world!" << std::endl; 
} 

int main() 
{ 
  boost::signal<void ()> s; 
  s.connect(func); 
  s(); 
} 
           

boost::signal 實際上被實作為一個模闆函數,具有被用作為事件處理器的函數的簽名,該簽名也是它的模闆參數。 在這個例子中,隻有簽名為 void () 的函數可以被成功關聯至信号 s。

函數 func() 被通過 connect() 方法關聯至信号 s。 由于 func() 符合所要求的 void () 簽名,是以該關聯成功建立。是以當信号 s 被觸發時,func() 将被調用。

信号是通過調用 s 來觸發的,就象普通的函數調用那樣。 這個函數的簽名對應于作為模闆參數傳入的簽名:因為 void () 不要求任何參數,是以括号内是空的。

調用 s 會引發一個觸發器,進而執行相應的 func() 函數 - 之前用 connect() 關聯了的。

同一例子也可以用 Boost.Function 來實作。

#include <boost/function.hpp> 
#include <iostream> 

void func() 
{ 
  std::cout << "Hello, world!" << std::endl; 
} 

int main() 
{ 
  boost::function<void ()> f; 
  f = func; 
  f(); 
} 
           

和前一個例子相類似,func() 被關聯至 f。 當 f 被調用時,就會相應地執行 func()。 Boost.Function 僅限于這種情形下适用,而 Boost.Signals 則提供了多得多的方式,如關聯多個函數至單個特定信号,示例如下。

#include <boost/signal.hpp> 
#include <iostream> 

void func1() 
{ 
  std::cout << "Hello" << std::flush; 
} 

void func2() 
{ 
  std::cout << ", world!" << std::endl; 
} 

int main() 
{ 
  boost::signal<void ()> s; 
  s.connect(func1); 
  s.connect(func2); 
  s(); 
} 
           

boost::signal 可以通過反複調用 connect() 方法來把多個函數指派給單個特定信号。 當該信号被觸發時,這些函數被按照之前用 connect() 進行關聯時的順序來執行。

另外,執行的順序也可通過 connect() 方法的另一個重載版本來明确指定,該重載版本要求以一個 int 類型的值作為額外的參數。

#include <boost/signal.hpp> 
#include <iostream> 

void func1() 
{ 
  std::cout << "Hello" << std::flush; 
} 

void func2() 
{ 
  std::cout << ", world!" << std::endl; 
} 

int main() 
{ 
  boost::signal<void ()> s; 
  s.connect(1, func2); 
  s.connect(0, func1); 
  s(); 
} 
           

和前一個例子一樣,func1() 在 func2() 之前執行。

要釋放某個函數與給定信号的關聯,可以用 disconnect() 方法。

#include <boost/signal.hpp> 
#include <iostream> 

void func1() 
{ 
  std::cout << "Hello" << std::endl; 
} 

void func2() 
{ 
  std::cout << ", world!" << std::endl; 
} 

int main() 
{ 
  boost::signal<void ()> s; 
  s.connect(func1); 
  s.connect(func2); 
  s.disconnect(func2); 
  s(); 
} 
           

這個例子僅輸出 Hello,因為與 func2() 的關聯在觸發信号之前已經被釋放。

除了 connect() 和 disconnect() 以外,boost::signal 還提供了幾個方法。

#include <boost/signal.hpp> 
#include <iostream> 

void func1() 
{ 
  std::cout << "Hello" << std::flush; 
} 

void func2() 
{ 
  std::cout << ", world!" << std::endl; 
} 

int main() 
{ 
  boost::signal<void ()> s; 
  s.connect(func1); 
  s.connect(func2); 
  std::cout << s.num_slots() << std::endl; 
  if (!s.empty()) 
    s(); 
  s.disconnect_all_slots(); 
} 
           

num_slots() 傳回已關聯函數的數量。如果沒有函數被關聯,則 num_slots() 傳回0。 在這種特定情況下,可以用 empty() 方法來替代。 disconnect_all_slots() 方法所做的實際上正是它的名字所表達的:釋放所有已有的關聯。

看完了函數如何被關聯至信号,以及弄明白了信号被觸發時會發生什麼事之後,還有一個問題:這些函數的傳回值去了哪裡? 以下例子回答了這個問題。

#include <boost/signal.hpp> 
#include <iostream> 

int func1() 
{ 
  return 1; 
} 

int func2() 
{ 
  return 2; 
} 

int main() 
{ 
  boost::signal<int ()> s; 
  s.connect(func1); 
  s.connect(func2); 
  std::cout << s() << std::endl; 
} 
           

func1() 和 func2() 都具有 int 類型的傳回值。 s 将處理兩個傳回值,并将它們都寫出至标準輸出流。 那麼,到底會發生什麼呢?

以上例子實際上會把 2 寫出至标準輸出流。 兩個傳回值都被 s 正确接收,但除了最後一個值,其它值都會被忽略。 預設情況下,所有被關聯函數中,實際上隻有最後一個傳回值被傳回。

你可以定制一個信号,令每個傳回值都被相應地處理。 為此,要把一個稱為合成器(combiner)的東西作為第二個參數傳遞給 boost::signal。

#include <boost/signal.hpp> 
#include <iostream> 
#include <algorithm> 

int func1() 
{ 
  return 1; 
} 

int func2() 
{ 
  return 2; 
} 

template <typename T> 
struct min_element 
{ 
  typedef T result_type; 

  template <typename InputIterator> 
  T operator()(InputIterator first, InputIterator last) const 
  { 
    return *std::min_element(first, last); 
  } 
}; 

int main() 
{ 
  boost::signal<int (), min_element<int> > s; 
  s.connect(func1); 
  s.connect(func2); 
  std::cout << s() << std::endl; 
} 
           

合成器是一個重載了 operator()() 操作符的類。這個操作符會被自動調用,傳入兩個疊代器,指向某個特定信号的所有傳回值。 以上例子使用了标準 C++ 算法 std::min_element() 來确定并傳回最小的值。

不幸的是,我們不可能把象 std::min_element() 這樣的一個算法直接傳給 boost::signal 作為一個模闆參數。 boost::signal 要求這個合成器定義一個名為 result_type 的類型,用于說明 operator()() 操作符傳回值的類型。 由于在标準 C++ 算法中缺少這個類型,是以在編譯時會産生一個相應的錯誤。

除了對傳回值進行分析以外,合成器也可以儲存它們。

#include <boost/signal.hpp> 
#include <iostream> 
#include <vector> 
#include <algorithm> 

int func1() 
{ 
  return 1; 
} 

int func2() 
{ 
  return 2; 
} 

template <typename T> 
struct min_element 
{ 
  typedef T result_type; 

  template <typename InputIterator> 
  T operator()(InputIterator first, InputIterator last) const 
  { 
    return T(first, last); 
  } 
}; 

int main() 
{ 
  boost::signal<int (), min_element<std::vector<int> > > s; 
  s.connect(func1); 
  s.connect(func2); 
  std::vector<int> v = s(); 
  std::cout << *std::min_element(v.begin(), v.end()) << std::endl; 
} 
           

這個例子把所有傳回值儲存在一個 vector 中,再由 s() 傳回。

4.3. 連接配接 Connections

函數可以通過由 boost::signal 所提供的 connect() 和 disconnect() 方法的幫助來進行管理。 由于 connect() 會傳回一個類型為 boost::signals::connection 的值,它們可以通過其它方法來管理。

#include <boost/signal.hpp> 
#include <iostream> 

void func() 
{ 
  std::cout << "Hello, world!" << std::endl; 
} 

int main() 
{ 
  boost::signal<void ()> s; 
  boost::signals::connection c = s.connect(func); 
  s(); 
  c.disconnect(); 
} 
           

boost::signal 的 disconnect() 方法需要傳入一個函數指針,而直接調用 boost::signals::connection 對象上的 disconnect() 方法則略去該參數。

除了 disconnect() 方法之外,boost::signals::connection 還提供了其它方法,如 block() 和 unblock()。

#include <boost/signal.hpp> 
#include <iostream> 

void func() 
{ 
  std::cout << "Hello, world!" << std::endl; 
} 

int main() 
{ 
  boost::signal<void ()> s; 
  boost::signals::connection c = s.connect(func); 
  c.block(); 
  s(); 
  c.unblock(); 
  s(); 
} 
           

以上程式隻會執行一次 func()。 雖然信号 s 被觸發了兩次,但是在第一次觸發時 func() 不會被調用,因為連接配接 c 實際上已經被 block() 調用所阻塞。 由于在第二次觸發之前調用了 unblock(),是以之後 func() 被正确地執行。

除了 boost::signals::connection 以外,還有一個名為 boost::signals::scoped_connection 的類,它會在析構時自動釋放連接配接。

#include <boost/signal.hpp> 
#include <iostream> 

void func() 
{ 
  std::cout << "Hello, world!" << std::endl; 
} 

int main() 
{ 
  boost::signal<void ()> s; 
  { 
    boost::signals::scoped_connection c = s.connect(func); 
  } 
  s(); 
} 
           

因為連接配接對象 c 在信号觸發之前被銷毀,是以 func() 不會被調用。

boost::signals::scoped_connection 實際上是派生自 boost::signals::connection 的,是以它提供了相同的方法。它們之間的差別僅在于,在析構 boost::signals::scoped_connection 時,連接配接會自動釋放。

雖然 boost::signals::scoped_connection 的确令自動釋放連接配接更為容易,但是該類型的對象仍需要管理。 如果在其它情形下連接配接也可以被自動釋放,而且不需要管理這些對象的話,就更好了。

#include <boost/signal.hpp> 
#include <boost/bind.hpp> 
#include <iostream> 
#include <memory> 

class world 
{ 
  public: 
    void hello() const 
    { 
      std::cout << "Hello, world!" << std::endl; 
    } 
}; 

int main() 
{ 
  boost::signal<void ()> s; 
  { 
    std::auto_ptr<world> w(new world()); 
    s.connect(boost::bind(&world::hello, w.get())); 
  } 
  std::cout << s.num_slots() << std::endl; 
  s(); 
} 
           

以上程式使用 Boost.Bind 将一個對象的方法關聯至一個信号。 在信号觸發之前,這個對象就被銷毀了,這會産生問題。 我們不傳遞實際的對象 w,而隻傳遞一個指針給 boost::bind()。 在 s() 被實際調用的時候,該指針所引向的對象已不再存在。

可以如下修改這個程式,使得一旦對象 w 被銷毀,連接配接就會自動釋放。

#include <boost/signal.hpp> 
#include <boost/bind.hpp> 
#include <iostream> 
#include <memory> 

class world : 
  public boost::signals::trackable 
{ 
  public: 
    void hello() const 
    { 
      std::cout << "Hello, world!" << std::endl; 
    } 
}; 

int main() 
{ 
  boost::signal<void ()> s; 
  { 
    std::auto_ptr<world> w(new world()); 
    s.connect(boost::bind(&world::hello, w.get())); 
  } 
  std::cout << s.num_slots() << std::endl; 
  s(); 
} 
           

如果現在再執行,num_slots() 會傳回 0 以確定不會試圖調用已銷毀對象之上的方法。 僅需的修改是讓 world 類繼承自 boost::signals::trackable。 當使用對象的指針而不是對象的副本來關聯函數至信号時,boost::signals::trackable 可以顯著簡化連接配接的管理。

繼續閱讀