# 第?4?章?事件處理
### 目錄
* [4.1 概述](eventhandling.html#eventhandling_general)
* [4.2 信號 Signals](eventhandling.html#eventhandling_signals)
* [4.3 連接 Connections](eventhandling.html#eventhandling_connections)
* [4.4 練習](eventhandling.html#eventhandling_exercises)
[](http://creativecommons.org/licenses/by-nc-nd/3.0/de/deed.zh) 該書采用 [Creative Commons License](http://creativecommons.org/licenses/by-nc-nd/3.0/de/deed.zh) 授權
## 4.1.?概述
很多開發者在聽到術語'事件處理'時就會想到GUI:點擊一下某個按鈕,相關聯的功能就會被執行。 點擊本身就是事件,而功能就是相對應的事件處理器。
這一模式的使用當然不僅限于GUI。 一般情況下,任意對象都可以調用基于特定事件的專門函數。 本章所介紹的 [Boost.Signals](http://www.boost.org/libs/signals) 庫提供了一個簡單的方法在 C++ 中應用這一模式。
嚴格來說,Boost.Function 庫也可以用于事件處理。 不過,Boost.Function 和 Boost.Signals 之間的一個主要區別在于,Boost.Signals 能夠將一個以上的事件處理器關聯至單個事件。 因此,Boost.Signals 可以更好地支持事件驅動的開發,當需要進行事件處理時,應作為第一選擇。
## 4.2.?信號 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();
}
```
* [下載源代碼](src/4.2.1/main.cpp)
`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();
}
```
* [下載源代碼](src/4.2.2/main.cpp)
和前一個例子相類似,`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();
}
```
* [下載源代碼](src/4.2.3/main.cpp)
`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();
}
```
* [下載源代碼](src/4.2.4/main.cpp)
和前一個例子一樣,`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();
}
```
* [下載源代碼](src/4.2.5/main.cpp)
這個例子僅輸出 `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();
}
```
* [下載源代碼](src/4.2.6/main.cpp)
`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;
}
```
* [下載源代碼](src/4.2.7/main.cpp)
`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;
}
```
* [下載源代碼](src/4.2.8/main.cpp)
合成器是一個重載了 `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;
}
```
* [下載源代碼](src/4.2.9/main.cpp)
這個例子把所有返回值保存在一個 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();
}
```
* [下載源代碼](src/4.3.1/main.cpp)
`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();
}
```
* [下載源代碼](src/4.3.2/main.cpp)
以上程序只會執行一次 `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();
}
```
* [下載源代碼](src/4.3.3/main.cpp)
因為連接對象 `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();
}
```
* [下載源代碼](src/4.3.4/main.cpp)
以上程序使用 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();
}
```
* [下載源代碼](src/4.3.5/main.cpp)
如果現在再執行,`num_slots()` 會返回 `0` 以確保不會試圖調用已銷毀對象之上的方法。 僅需的修改是讓 `world` 類繼承自 `boost::signals::trackable`。 當使用對象的指針而不是對象的副本來關聯函數至信號時,`boost::signals::trackable` 可以顯著簡化連接的管理。
## 4.4.?練習
You can buy [solutions to all exercises](http://en.highscore.de/shop/index.php?p=boost-solution) in this book as a ZIP file.
1. 編寫一個程序,定義一個名為 `button` 的類,表示GUI中的一個可點擊按鈕。 為該類加入兩個方法 `add_handler()` 和 `remove_handler()`,它們均要求一個函數名作為參數。 如果 `click()` 方法被調用,已登記的函數將被按順序執行。
如下測試你的代碼,創建一個 `button` 類的實例,從事件處理器內部向標準輸出流寫出一個信息。 調用 `click()` 函數模擬用鼠標點擊該按鈕。