## 用法
當你面對需要用多段代碼來處理一個事件的情況時,典型的解決方案有:用函數指針進行回調,或者直接對 產生事件的子系統與處理事件的子系統之間的依賴性進行編碼。這種設計常常會導致循環的依賴性。通過使用 Boost.Signals, 你將獲得靈活性和解耦。要開始使用這個庫,首先要包含頭文件 `"boost/signals.hpp"`.\[2\]
<small class="calibre23"></small><small class="calibre23">??? ??? [2]</small> <small class="calibre23"></small><small class="calibre23"></small><small class="calibre23">Boost.Signals</small> 庫和 <small class="calibre40"></small><small class="calibre40">Boost.Regex</small> 庫是本書所討論的庫中僅有的需要編譯和鏈接才能使用的庫。編譯的過程很簡單,在線文檔中已有詳盡的描述,這里我不再復述。
以下例子示范了 `signal`s 和插槽(slots)的基本特性,包括如何連接它們以及如何產生一個 `signal`. 注意,插槽指的是由你提供的一個兼容于 `signal` 的函數簽名的函數或函數對象。在以下代碼中,我們既創建了一個普通函數,`my_first_slot`, 也創建了一個函數對象,`my_second_slot`; 它們兩個都將連接到我們創建的一個 `signal` 上。
```
#include <iostream>
#include "boost/signals.hpp"
void my_first_slot() {
std::cout << "void my_first_slot()\n";
}
class my_second_slot {
public:
void operator()() const {
std::cout <<
"void my_second_slot::operator()() const\n";
}
};
int main() {
boost::signal<void ()> sig;
sig.connect(&my_first_slot);
sig.connect(my_second_slot());
std::cout << "Emitting a signal...\n";
sig();
}
```
我們首先聲明一個 `signal`, 它所需的插槽為返回 `void` 且不帶參數。然后,我們把兩個兼容的插槽類型連接到該 `signal`. 對于第一個插槽,我們用普通函數 `my_first_slot` 的地址調用 `connect`。對于另一個插槽,我們缺省構造一個函數對象 `my_second_slot` 的實例并把它傳給 `connect`。這些連接意味著當我們產生一個 `signal` (通過調用 `sig`)時,這兩個插槽將被立即調用。
```
sig();
```
運行這個程序,輸出信息如下:
```
Emitting a signal...
void my_first_slot()
void my_second_slot::operator()() const
```
但是,后兩行的順序不一定是這樣的,因為屬于同一個組的插槽會以不確定的順序執行。沒有辦法確定哪一個插槽會先被調用。如果插槽的調用順序事關緊要,你就必須把它們放入不同的組。
### 插槽分組
有時候,某些插槽需要在其它插槽之前調用,例如某些插槽會產生一些副作用而別的插槽需要依賴于這些副作用。分組就是支持這種需求的方法。`signal` 有一個模板參數,名為 `Group`,其缺省值為 `int`.?Groups 缺省以 `std::less<Group>` 為排序標準,對于 `int` 就是 `operator<` 。換句話說,屬于 group 0 的插槽會在 group 1 的插槽之前調用,等等。但是請注意,同一個組中的插槽的調用順序是不確定的。要嚴格控制所有插槽的調用順序,唯一的辦法就是把每個插槽都安排到各自的組中。
把插槽指定到一個組的方法是,傳遞一個 `Group` 給 `signal::connect`. 一個已連接插槽不能改變其所屬的組;要改變一個插槽所屬的組,必須先斷開它的連接,然后重新把它連接到 `signal` 上并同時指定新組。
作為例子,我們考慮兩個插槽,它們帶一個類型為 `int&` 的參數;第一個插槽將參數加倍,第二個插槽則把當前值加3。我們要求正確的語義是,先把該值加倍,然后再加3。如果不指定順序,我們就不能確保按該語義執行。以下方法只能在某些系統的某些時候正確執行(可能是周一或周三而且月圓的時候)。
```
#include <iostream>
#include "boost/signals.hpp"
class double_slot {
public:
void operator()(int& i) const {
i*=2;
}
};
class plus_slot {
public:
void operator()(int& i) const {
i+=3;
}
};
int main() {
boost::signal<void (int&)> sig;
sig.connect(double_slot());
sig.connect(plus_slot());
int result=12;
sig(result);
std::cout << "The result is: " << result << '\n';
}
```
運行這段程序,可能產生以下輸出:
```
The result is: 30
```
或者產生以下輸出:
```
The result is: 27
```
不使用分組的方法就無法保證正確的行為。我們需要確保 `double_slot` 總是在 `plus_slot` 之前被調用。這就要求我們要指定 `double_slot` 屬于一個順序在 `plus_slot` 所屬的組之前的組,即:
```
sig.connect(0,double_slot());
sig.connect(1,plus_slot());
```
這樣可以確保得到我們想要的(即 27)。再次提醒,對于同一個組中的插槽,它們被調用的順序是不確定的。只要你需要插槽以特定的順序來執行,就必須確保它們使用不同的組。
`Groups` 的類型是類 `signal` 的一個模板參數,所以它可以使用別的類型,如 `std::string` 。
```
#include <iostream>
#include <string>
#include "boost/signals.hpp"
class some_slot {
std::string s_;
public:
some_slot(const std::string& s) : s_(s) {}
void operator()() const {
std::cout << s_ << '\n';
}
};
int main() {
boost::signal<void (),
boost::last_value<void>,std::string> sig;
some_slot s1("I must be called first, you see!");
some_slot s2("I don't care when you call me, not at all. \
It'll be after those belonging to groups, anyway.");
some_slot s3("I'd like to be called second, please.");
sig.connect(s2);
sig.connect("Last group",s3);
sig.connect("First group",s1);
sig();
}
```
首先我們定義一個插槽類型,它在執行時輸出一個 `std::string` 到 `std::cout` 。然后,我們聲明 `signal`. 因為 `Groups` 參數是在 `Combiner` 類型之后的,所以我們必須同時指定 `Combiner` (我們只是按缺省值來聲明)。我們把 `Groups` 類型設為 `std::string` 。
```
boost::signal<void (),boost::last_value<void>,std::string> sig;
```
對于剩下的模板參數,我們接受缺省值就可以了。在連接到插槽 `s1`, `s2`, 和 `s3` 時,所創建的組是以字母順序排序的(因為這是 `std::less<std::string>` 的行為),因此 `"First group"` 先于 `"Last group"`. 注意,由于字符串常量可以隱式轉換為 `std::string`, 所以我們可以把它們直接傳遞給 `signal` 的 `connect` 函數。運行該程序可以告訴我們正確的結果。
```
I must be called first, you see!
I'd like to be called second, please.
I don't care when you call me, not at all.
It'll be after those belonging to groups, anyway.
```
我們也可以在聲明 `signal` 類型時選擇別的排序方法,例如 `std::greater`.
```
boost::signal<void (),boost::last_value<void>,
std::string,std::greater<std::string> > sig;
```
如果我們把它用于前面的例子,輸出將變為:
```
I'd like to be called second, please.
I must be called first, you see!
I don't care when you call me, not at all.
It'll be after those belonging to groups, anyway.
```
當然,在這個例子中,`std::greater` 產生的順序導致了錯誤的輸出,但這是另一回事。分組非常有用,絕對必要,但是給組賦以正確的值并不總是那么簡單的事,因為被連接的插槽并不需要在代碼的同 一個地方執行。弄清楚某個插槽所應該使用什么組號可能是個問題。有時,這個問題可以用規定來解決,即在代碼中增加注釋,確保每個人都能看到這些注釋,但是 這也只能在代碼中不是很多地方要進行組號的賦值以及程序員不偷懶時有用。換句話說,這種方法也不一定管用。所以,你需要一個集中的產生組號的地方,它可以 依據某個給定的值為每個插槽產生唯一的組號,或者如果相關的插槽相互了解,那么也可以由插槽提供它們自己的組號。
現在你已經知道如何解決按順序調用插槽的問題了,讓我們來看看如何讓你的 `signal`s 使用不同的簽名。你常常需要傳遞額外的信息給你系統中的重要事件。
### 帶參數的 Signals
通常會有一些額外的數據要傳遞給 `signal`. 例如,想象一個溫度保護器,它報告溫度的急劇變化。僅僅知道保護器發現了問題是不夠的;插槽可能需要知道當前的溫度。雖然保護器(一個 `signal`)和插槽都可以從一個公用的傳感器去獲取溫度值,但是最簡單的方式還是讓保護器在調用插槽時把當前溫度傳遞給插槽。還有一個例子,想象有多個插槽連接到多個 `signal` 上:插槽很可能需要知道是哪一個 `signal` 調用了它。有很多用例都需要從 `signal` 傳遞一些信息給插槽。插槽接受的參數是 `signal` 聲明中的一部分。`signal` 類模板的第一個參數就是調用 `signal` 的函數簽名,而且這個簽名也用于 `signal` 調用那些被連接的插槽。如果我們想這個參數可以修改,我們就要確保它是通過非`const` 引用或指針來進行傳遞的,否則我們就可以通過值或 `const` 引用來傳遞它。注意,這個原始參數除了是可修改或不可修改這么明顯的差異之外,對于 `signal` 本身以及插槽可以接受的參數類型還有一些隱喻,如果 `signal` 接受一個傳值或傳 `const` 引用的參數,那么所有可以隱式轉換為該參數類型的類型都可以用于產生一個 `signal`. 對于插槽也一樣,如果插槽是通過傳值或傳 `const` 引用來接受參數的話,這就意味著允許從 `signal` 的真正參數類型隱式轉換到這個類型。我們后面將討論如果在處理信號時正確地傳遞參數,屆時我們將看到更多關于這一點的詳細討論。
想象一個自動停車場監視器,一旦有車進入或離開停車場,監視器將收到一個通知。它需要知道一些關于這 輛車的唯一信息,例如車的登記號碼,這樣它才可以跟蹤每輛車的進入和離開。這個監視器有一個它自己的 `signal`,能夠在有人試圖進行欺騙時觸發警報。這樣就需要一些警衛監聽這個 `signal`, 我們用一個名為 `security_guard` 來對它們進行建模。最后,我們再增加一個 `gate` 類,它包含一個 `signal` 用于在一輛車進入或離開停車場時產生。( `parking_lot_guard` 顯然需要知道這一點)。我們先來看看這個 `parking_lot_guard` 的聲明。
```
class parking_lot_guard {
typedef
boost::signal<void (const std::string&)> alarm_type;
typedef alarm_type::slot_type slot_type;
boost::shared_ptr<alarm_type> alarm_;
typedef std::vector<std::string> cars;
typedef cars::iterator iterator;
boost::shared_ptr<cars> cars_;
public:
parking_lot_guard();
boost::signals::connection
connect_to_alarm(const slot_type& a);
void operator()(bool is_entering,const std::string& car_id);
private:
void enter(const std::string& car_id);
void leave(const std::string& car_id);
};
```
這里有三個特別重要的地方要認真看一下;第一個是警報,即一個返回 `void` 且接受一個 `std::string` (它用于標識一輛車)的 `boost::signal` 。這個 `signal` 的聲明值得再好好看一次。
```
boost::signal<void (const std::string&)>
```
它就象是一個函數的聲明,只是沒有了函數名。如果有懷疑,請記住除此以外沒有別的東西了!你可以從外部使用成員函數 `connect_to_alarm` 連接這個 `signal` 。(我們將看到在實現這個類時,如何以及為何我們要發出警報)。下一個要留意的地方是,這個警報以及容納車輛標識的容器(一個容納 `std::string`s 的 `std::vector`?)兩者均保存于 `boost::shared_ptr` 中。這樣做的原因是,盡管我們只是打算聲明一個 `parking_lot_guard` 實例,但是也可能變成多份拷貝;因為這個監視器類稍后還會連接到其它的 `signal` 上,這樣就會創建多份拷貝(Boost.Signals 會復制插槽,所以需要正確地管理生存期);而我們希望所有的數據都可用,因此我們就要共享它。雖然我們可以避免拷貝,例如通過使用指針或者把插槽的行為外 部化,但是這樣做可以發現一些容易掉進去的陷阱。最后還要留意的是,我們聲明了一個調用操作符,其原因是我們將要在 `gate` 類(待會定義)中把 `parking_lot_guard` 連接到一個 `signal` in the class?.
現在讓我們把注意力放到 `security_guard` 類。
```
class security_guard {
std::string name_;
public:
security_guard (const char* name);
void do_whatever_it_takes_to_stop_that_car() const;
void nah_dont_bother() const;
void operator()(const std::string& car_id) const;
};
```
`security_guard`s 并不需要做太多事情。這個類有一個調用操作符,用作來自于 `parking_lot_guard` 的警報的一個插槽,另外還有兩個函數:一個用于停住引發警報的車輛,另一個不做任何事。下面帶來我們的 `gate` 類,它用于在有車輛到達停車場以及車輛離開時進行檢查。
```
class gate {
typedef
boost::signal<void (bool,const std::string&)> signal_type;
typedef signal_type::slot_type slot_type;
signal_type enter_or_leave_;
public:
boost::signals::connection
connect_to_gate(const slot_type& s);
void enter(const std::string& car_id);
void leave(const std::string& car_id);
};
```
你將留意到,`gate` 類包含一個 `signal`,它在有車輛進入或離開停車場時被觸發。有一個公用成員函數(`connect_to_gate`)用于連接這個 `signal`, 另兩個成員函數(`enter` 和 `leave`)用于在車輛進入或離開時被調用。
現在是時候來實現它們了。讓我們從 `gate` 類開始。
```
class gate {
typedef
boost::signal<void (bool,const std::string&)> signal_type;
typedef signal_type::slot_type slot_type;
signal_type enter_or_leave_;
public:
boost::signals::connection
connect_to_gate(const slot_type& s) {
return enter_or_leave_.connect(s);
}
void enter(const std::string& car_id) {
enter_or_leave_(true,car_id);
}
void leave(const std::string& car_id) {
enter_or_leave_(false,car_id);
}
};
```
這個實現很簡單。多數工作都前轉到其它對象。函數 `connect_to_gate` 簡單地把調用轉為對 `signal enter_or_leave_` 的 `connect` 的調用。函數 `enter` 產生 `signal`, 傳入一個 `true` (代表有車輛進入)和車輛的標識。`leave` 完成同樣的工作,但是傳入的是 `false`, 代表有車輛離開。簡單的類做簡單的事。`security_guard` 類也不太復雜。
```
class security_guard {
std::string name_;
public:
security_guard (const char* name) : name_(name) {}
void do_whatever_it_takes_to_stop_that_car() const {
std::cout <<
"Stop in the name of...eh..." << name_ << '\n';
}
void nah_dont_bother() const {
std::cout << name_ <<
" says: Man, that coffee tastes f i n e fine!\n";
}
void operator()(const std::string& car_id) const {
if (car_id.size() && car_id[0]=='N')
do_whatever_it_takes_to_stop_that_car();
else
nah_dont_bother();
}
};
```
`security_guard`s 知道它們自己的名字,并且可以決定在警報發出時是否要做些事情(如果 `car_id` 以字母 N 打頭,它們就會有所動作)。調用操作符就是被調用的插槽函數,`security_guard` 對象是一個函數對象,并且符合 `parking_lot_guard` 的 `alarm_type` 信號的要求。`parking_lot_guard` 稍微復雜一些,但也不是很復雜。
```
class parking_lot_guard {
typedef
boost::signal<void (const std::string&)> alarm_type;
typedef alarm_type::slot_type slot_type;
boost::shared_ptr<alarm_type> alarm_;
typedef std::vector<std::string> cars;
typedef cars::iterator iterator;
boost::shared_ptr<cars> cars_;
public:
parking_lot_guard()
: alarm_(new alarm_type), cars_(new cars) {}
boost::signals::connection
connect_to_alarm(const slot_type& a) {
return alarm_->connect(a);
}
void operator()
(bool is_entering,const std::string& car_id) {
if (is_entering)
enter(car_id);
else
leave(car_id);
}
private:
void enter(const std::string& car_id) {
std::cout <<
"parking_lot_guard::enter(" << car_id << ")\n";
// 如果車輛已經在這,就觸發警報
if (std::binary_search(cars_->begin(),cars_->end(),car_id))
(*alarm_)(car_id);
else // Insert the car_id
cars_->insert(
std::lower_bound(
cars_->begin(),
cars_->end(),car_id),car_id);
}
void leave(const std::string& car_id) {
std::cout <<
"parking_lot_guard::leave(" << car_id << ")\n";
// 如果是未登記的車輛,就觸發警報
std::pair<iterator,iterator> p=
std::equal_range(cars_->begin(),cars_->end(),car_id);
if (p.first==cars_->end() || *(p.first)!=car_id)
(*alarm_)(car_id);
else
cars_->erase(p.first);
}
};
```
就是這樣了!(當然,我們還沒有把插槽連接到 `signal` 上,還要做一些事情。但是這些類對于所要做的事情而言還是非常地簡單的)。 為了讓警報和車輛標識的 `shared_ptr` 有正確的行為,我們實現了缺省構造函數,在其中適當地分配了 `signal` 和 `vector` 。隱式創建的復制構造函數、析構函數以及賦值操作符都可以正確工作(這要歸功于智能指針)。函數 `connect_to_alarm` 把調用轉到所含的 `signal` 的 `connect`. 調用操作符則檢查其布爾參數的值來看是否有車輛進入或離開,并且調用相應的函數 `enter` 或 `leave`. 在函數 `enter` 中,首先做的是在車輛標識的 `vector` 中進行查找。如果找到該標識則說明有問題;可能有人偷了車號牌。查找采用的是算法 `binary_search`,\[3\] 它要求容器是有序的(我們必須要確保它總是有序的)。如果我們發現標識已存在,就立即觸發警報,即調用 `signal` 。
<small class="calibre23"></small><small class="calibre23">??? ??? [3]?</small><small class="calibre40"></small><small class="calibre40">`binary_search`</small> 的復雜度為 <small class="calibre23"></small><small class="calibre23"></small><small class="calibre23">`O(logN)`.</small>
```
(*alarm_)(car_id);
```
首先我們需要解引用 `alarm_` ,因為 `alarm_` 是一個 `boost::shared_ptr`, 而在調用它時,我們傳給它一個表示車輛標識的參數。如果我們沒有找到該標識,則一切正常, 我們就把這個車輛標識插入到 `cars_` 的正確位置中。記住我們必須保證容器隨時有序,最好的辦法就是把元素插入到一個不會影響順序的位置上。算法 `lower_bound` 可以給我們指出這個位置(該算法同樣要求有序序列)。最后一個是函數 `leave`, 它在有車輛離開停車場時被調用。`leave` 先確認車輛的標識是否已登記在我們的容器中。這是通過調用算法 `equal_range` 來實現的,該算法返回一對迭代器,表示了一個元素可以插入且不影響有序性的范圍。這意味著我們必須解引用這個返回的迭代器并確認它的值是否等于我們要查找的那個。如果我們沒有找到,我們就要再一次觸發警報,而如果我們找到了,就只需要簡單地把它從 `vector` 中刪掉。你也許留意到我們沒有給出停車者交費的代碼;這種有害的代碼超出了本書的范圍。
我們的停車場所需的各個參與者都已經定義好了,我們必須連接這些 `signal`s 和這些插槽,否則不會發生任何事情!`gate` 類不知道任何關于 `parking_lot_guard` 類的東西,同樣后者也不知道任何關于 `security_guard` 類的東西。這就是本庫的一個特性:產生事件的類型不需要對接收事件的類型有任何了解。回到這個例子上,我們來看看是否可以讓這個停車場運作起來。
```
int main() {
// 創建一些警衛
std::vector<security_guard> security_guards;
security_guards.push_back("Bill");
security_guards.push_back("Bob");
security_guards.push_back("Bull");
// 創建兩個門
gate gate1;
gate gate2;
// 創建自動監視器
parking_lot_guard plg;
// 把自動監視器連接到門上
gate1.connect_to_gate(plg);
gate2.connect_to_gate(plg);
// 把警衛連接到自動監視器上
for (unsigned int i=0;i<security_guards.size();++i) {
plg.connect_to_alarm(security_guards[i]);
}
std::cout << "A couple of cars enter...\n";
gate1.enter("SLN 123");
gate2.enter("RFD 444");
gate2.enter("IUY 897");
std::cout << "\nA couple of cars leave...\n";
gate1.leave("IUY 897");
gate1.leave("SLN 123");
std::cout << "\nSomeone is entering twice - \
or is it a stolen license plate?\n";
gate1.enter("RFD 444");
}
```
這就是你要的,一個具有完整功能的停車場。我們創建了三個 `security_guard`s, 兩個 `gate`s, 和一個 `parking_lot_guard`. 它們相互之間一無所知,但我們還是要通過正確的架構把它們聯系起來,停車場中發生的重要事件才得以相互傳遞。這意味著要把 `parking_lot_guard` 連接到兩個 `gate`s 上。
```
gate1.connect_to_gate(plg);
gate2.connect_to_gate(plg);
```
這樣就確保了無論何時 `gate` 實例中產生了 `signal enter_or_leave_` 信號,`parking_lot_guard` 都可以收到這個事件通知。接著,我們再將 `security_guard`s 連接到 `parking_lot_guard` 中的警報 `signal` 上。
```
plg.connect_to_alarm(security_guards[i]);
```
我們已經設法將這些類型相互之間進行了解耦,它們還是得到了執行它們的職責所需的適量的信息。在前面的代碼中,我們讓少量的車輛進入和離開,來測試這個停車場。這個真實世界的模擬顯示了我們已經讓各個模塊按要求相互通信了。
```
A couple of cars enter...
parking_lot_guard::enter(SLN 123)
parking_lot_guard::enter(RFD 444)
parking_lot_guard::enter(IUY 897)
A couple of cars leave...
parking_lot_guard::leave(IUY 897)
parking_lot_guard::leave(SLN 123)
Someone is entering twice - or is it a stolen license plate?
parking_lot_guard::enter(RFD 444)
Bill says: Man, that coffee tastes f.i.n.e fine!
Bob says: Man, that coffee tastes f.i.n.e fine!
Bull says: Man, that coffee tastes f.i.n.e fine!
```
可惜的是,拿著車牌 RFD 444 的騙子跑掉了,但是你能做的就是這些。
關于 `signal`s 的參數已經討論了很長一段篇幅,事實上我們更多是在討論 Signals 的基本用法,即對產生 `signal`s 的類型和監聽它的插槽進行解耦。記住,任何類型的參數都可以傳遞,而 `signal` 類型的聲明決定了插槽函數的簽名,該聲明看起來就象一個不帶函數名的函數聲明。我們根本沒有提到返回類型,雖然它也是簽名的一部分。這個疏忽的原因是返回類型可以有多種不同的處理方法,接下來我們將看到為什么會這樣以及如何去做。
### 對結果進行組合
如果一個 `signal` 的簽名以及它的插槽具有非`void` 的返回類型,顯然對于插槽的返回值會有事發生,事實上,那個對 `signal` 的調用將產生某種結果。但是結果是什么呢?`signal` 類模板有一個參數名為 Combiner, 它就是負責組合并返回結果的一個類型。缺省的 Combiner 是 `boost::last_value`, 它是一個類,只負責簡單地返回所調用的最后一個插槽的返回值。那么,究竟是哪一個插槽呢?我們真的不知道,因為調用同一個組內的插槽的順序是不確定的\[4\]。我們從一個小例子來示范一下缺省的 Combiner 。
> \[4\] 所以,假設最后一個組中只有一個插槽,我們就可以知道。
```
#include <iostream>
#include "boost/signals.hpp"
bool always_return_true() {
return true;
}
bool always_return_false() {
return false;
}
int main() {
boost::signal<bool ()> sig;
sig.connect(&always_return_true);
sig.connect(&always_return_false);
std::cout << std::boolalpha << "True or false? " << sig();
}
```
有兩個插槽,`always_return_true` 和 `always_return_false`, 被連接到 `signal sig`, 每個都返回一個 `bool` 且不帶參數。調用 `sig` 的結果被輸出到 `cout`. 它會是 `true` 還是 `false`? 不經測試的話,我們無法知道(我試了一上,結果是 `false`)。在實踐中,你要么不關心調用 `signal` 所返回的值,要么你就要創建你自己的 Combiner 來提供有意義的、客戶化的行為。例如,可能是對所有插槽返回的結果進行處理后得到調用 `signal` 的最終結果。另一種情況,也可能是在某一個插槽返回 `false` 后就不再調用其它的插槽。一個定制的 Combiner 可以做到這些,甚至更多。這是因為 Combiner 可以對插槽進行逐個調用,并根據返回值來決定做什么。
想象一個初始化序列,其中任何失敗都將中止整個序列。插槽可以根據它們被調用的次序來指定到組中。沒有一個定制的 Combiner 的話,它看起來就象這樣:
```
#include <iostream>
#include "boost/signals.hpp"
bool step0() {
std::cout << "step0 is ok\n";
return true;
}
bool step1() {
std::cout << "step1 is not ok. This won't do at all!\n";
return false;
}
bool step2() {
std::cout << "step2 is ok\n";
return true;
}
int main() {
boost::signal<bool ()> sig;
sig.connect(0,&step0);
sig.connect(1,&step1);
sig.connect(2,&step2);
bool ok=sig();
if (ok)
std::cout << "All system tests clear\n";
else
std::cout << "At least one test failed. Aborting.\n";
}
```
以上這段代碼沒有辦法讓代碼知道其中有一個測試是失敗的。你也記得,缺省的 combiner 是 `boost::last_value`, 它只是簡單地返回最后一個插槽的返回值,即調用 `step2` 的返回值。運行這個例子會給出一個令人失望的輸出:
```
step0 is ok
step1 is not ok. This won't do at all!
step2 is ok
All system tests clear
```
顯然這不是正確的結果。我們需要一個 Combiner ,它應該在某個插槽返回 `false` 時中止處理,并把結果傳回給 `signal`. 一個 Combiner 就是一個具有某些額外要求的函數對象。它必須有一個名為 `result_type` 的 `typedef`,用于指定其調用操作符的返回類型。此外,調用操作符必須以它被調用的迭代器類型泛化。我們這里需要的 Combiner 非常簡單,因此它恰好是一個好的例子。
```
class stop_on_failure {
public:
typedef bool result_type;
template <typename InputIterator>
bool operator()(InputIterator begin,InputIterator end) const
{
while (begin!=end) {
if (!*begin)
return false;
++begin;
}
return true;
}
};
```
注意,公有的 `typedef result_type`, 它定義為 `bool`. `result_type` 的類型無需與插槽的返回類型相關。(在聲明 `signal` 時,你指定了插槽的簽名以及 `signal` 的調用操作符的參數。但是,Combiner 的返回類型決定了 `signal` 的調用操作符的返回類型。缺省情況下,它與插槽的返回類型相同,但這不是必須的)。`stop_on_failure` 的調用操作符以一個插槽迭代器類型所泛化,它對插槽進行逐個迭代并調用;直到我們遇到一個錯誤為止。對于 `stop_on_failure`, 我們不想在遇到錯誤的返回值后再繼續調用插槽,因此我們對于每次調用都檢查其返回值。如果返回值為 `false`, 該函數說立即返回,否則它繼續調用下一個插槽。要使用這個 `stop_on_failure`, 我們只需在聲明 `signal` 類型時指出即可:
```
boost::signal<bool (),stop_on_failure> sig;
```
如果我們在前面的例子中使用它,則輸出的結果就會符合我們的要求了。
```
step0 is ok
step1 is not ok. This won't do at all!
At least one test failed. Aborting.
```
Combiner 的另一個常用類型是,返回所有被調用插槽的返回值中的最大或最小值。還有其它很多有趣的 Combiners,包括:將所有結果保存在一個容器中。本庫的(優秀的)在線文檔就有這么一個 Combiner 的例子,你應該去讀一下!你并不是每天都需要編寫自己的 Combiner 類,但偶爾在為一個復雜的問題給出一個漂亮的解決方案時可能會用到。
### Signals 決不能復制
我已經提到過,`signal`s 不能被復制,但是值得留意的是,應該怎樣實現一個包含 `signal` 的類。這些類也都必須是不可復制的嗎?不,它們不必,但必須手工實現其復制構造函數和賦值操作符。因為 `signal` 類將其復制構造函數和賦值操作符聲明為私有的,所以一個聚合了 `signal`s 的類必須實現其所需的語義。正確處理復制的一個方法是,在類的多個實例間共享 `signal`s,我們在停車場的例子中就是這么做的。在那個例子中,每一個 `parking_lot_guard` 實例通過 `boost::shared_ptr` 引向同一個 `signal`。對于其它類,可以在拷貝中缺省構造 `signal`,因為該復制語義不包含對插槽的連接。另一種情況是,復制一個含有 `signal` 的類是沒有意義的,這種情況下你可以依賴所含 `signal` 的不可復制語義來確保復制與賦值是被禁止的。為了看得更清楚一點,考慮一個類 `some_class`, 它的定義是:
```
class some_class {
boost::signal<void (int)> some_signal;
};
```
對于這個類,編譯器生成的復制構造函數和賦值操作符都是不能使用的。如果代碼企圖去使用它們,編譯器就會抗議。例如,以下例子試圖從 `sc1` 復制構造 `some_class sc2` :
```
int main() {
some_class sc1;
some_class sc2(sc1);
}
```
編譯這段程序時,編譯器生成的復制構造函數試圖對 `some_class` 的成員進行逐個成員的復制。由于 `signal` 的私有復制構造函數,編譯器會輸出以下信息:
```
c:/boost_cvs/boost/boost/noncopyable.hpp: In copy constructor `
boost::signals::detail::signal_base::signal_base(const
boost::signals::detail::signal_base&)':
c:/boost_cvs/boost/boost/noncopyable.hpp:27: error: `
boost::noncopyable::noncopyable(
const boost::noncopyable&)' is private
noncopyable_example.cpp:10: error: within this context
```
所以,無論你的含有 `signal` 的類需要哪一種復制和賦值,你都必須確保其中不會有對 `signal` 的復制!
### 管理連接
我們已經討論了如何連接插槽到 `signal`s, 但我們還沒有看到如何斷開它們。有許多原因讓一個插槽不應該永久地連接到一個 `signal` 上。到現在為止,我們都忽略了它,其實 `boost::signal::connect` 會返回一個 `boost::signals::connection` 實例。通過使用這個 `connection` 對象,就可以從 `signal` 斷開一個插槽,也可以測試一個插槽是否已連接到 `signal`. `connection` 是到 `signal` 和插槽間的實際鏈接的一個句柄。由于 `signal` 和插槽間的連接的信息是由它們兩者分別跟蹤的,所以插槽并不知道它本身是否被連接。如果一個插槽不想與 `signal` 斷開,它只要忽略掉 `signal::connect` 所返回的 `connection` 即可。還有,對一個插槽所屬的組調用 `disconnect`,或者調用 `disconnect_all_slots` 都會斷開插槽而無需提供插槽的 `connection`. 如果檢查插槽是否還連接著 `signal` 的能力非常重要,你就只能保存 `connection` 并用它來詢問 `signal`,別無它法。
`connection` 類提供了 `operator<`, 這使得你可以把連接保存在標準庫的容器中。為了完備性,它也提供了 `operator==` 。最后,這個類提供了一個 `swap` 成員函數,用于與另一個 `connection` 交換各自的 `signal`/slot 連接信息。以下例子示范了如何使用 `signals::connection` 類:
```
#include <iostream>
#include <string>
#include "boost/signals.hpp"
class some_slot_type {
std::string s_;
public:
some_slot_type(const char* s) : s_(s) {}
void operator()(const std::string& s) const {
std::cout << s_ << ": " << s << '\n';
}
};
int main() {
boost::signal<void (const std::string&)> sig;
some_slot_type sc1("sc1");
some_slot_type sc2("sc2");
boost::signals::connection c1=sig.connect(sc1);
boost::signals::connection c2=sig.connect(sc2);
// 比較
std::cout << "c1==c2: " << (c1==c2) << '\n';
std::cout << "c1<c2: " << (c1<c2) << '\n';
// 檢查連接
if (c1.connected())
std::cout << "c1 is connected to a signal\n";
// 交換并斷開
sig("Hello there");
c1.swap(c2);
sig("We've swapped the connections");
c1.disconnect();
sig("Disconnected c1, which referred to sc2 after the swap");
}
```
在這個例子中有兩個 `connection` 對象,我們看到它們可以用 `operator<` 和 `operator==` 來比較。`operator<` 所實現的順序關系是不確定的;它的存在是為了支持把 `connection`s 保存到標準庫的容器中。而 `operator==` 所表示的等價關系則是有定義的。如果兩個 `connection`s 引向同一個物理連接,它們就是等價的。如果兩個 `connection`s 不引向任何連接,它們也是等價的。其它的 `connection`s 對都不等價。在這個例子中,我們還斷開了一個 `connection`.
```
c1.disconnect();
```
雖然 `c1` 原先是引向 `sc1` 的 `connection`,但是在斷開的時候它是引向 `sc2` 的,因為我們用成員函數 `swap` 交換了這兩個連接的內容。斷開連接意味著在 `signal` 產生時,該插槽不再被通知。以下是該程序的運行結果:
```
c1==c2: 0
c1<c2: 1
c1 is connected to a signal
sc1: Hello there
sc2: Hello there
sc1: We've swapped the connections
sc2: We've swapped the connections
sc1: Disconnected c1, which referred to sc2 after the swap
```
如你所見,最后一次的 `signal sig` 只調用了插槽 `sc1`.
有些時候,一個插槽的 `connection` 的生存期只限于某一段特定代碼的范圍。這種情況類似于其它資源要求僅限于某個特定范圍時,通常可以使用智能指針或其它作用域機制來處理。Boost.Signals 提供了 `connection` 的一個作用域版本,名為 `scoped_connection`. `scoped_connection` 確保該 `connection` 在 `scoped_connection` 被銷毀時斷開連接。`scoped_connection` 的構造函數用一個 `connection` 對象作參數,它以此方式接受其所有權。
```
#include <iostream>
#include "boost/signals.hpp"
class slot {
public:
void operator()() const {
std::cout << "Something important just happened!\n";
}
};
int main() {
boost::signal<void ()> sig;
{
boost::signals::scoped_connection s=sig.connect(slot());
}
sig();
}
```
`boost::signals::scoped_connection s` 被限定在 `main` 內的一個小范圍中,在離開該范圍后,`signal sig` 被調用。這里不會產生輸出,因為 `scoped_connection` 已經斷開了插槽與 `signal` 間的連接。使用這樣的帶作用域的資源可以簡化代碼及其維護工作。
### 用 Bind 和 Lambda 創建插槽
你已經看到 Signals 多么有用以及么靈活。但是,當你把 Boost.Signals 與 Boost.Bind 和 Boost.Lambda 結合使用時,你會發現更大的威力。這兩個庫,它們的詳細討論請見 "[Library 9](../Text/content.html#ch09): [Bind 9](../Text/content.html#ch09)" 和 "[Library 10](../Text/content.html#ch10): [Lambda 10](../Text/content.html#ch10)",它們有助于就地創建函數對象。這意味著你可以在需要連接到 `signal` 的地方就地創建插槽(以及插槽類型),不再需要為插槽編寫一個特定的、功能單一的類,然后再創建一個實例并連接它。這樣做還可以把插槽的邏輯就放在使用它 們的地方,而不是放在源代碼的別的地方。最后,這些庫甚至可以用于改編一些已有的庫,這些已有的庫不提供調用操作符,但是有別的合適的方法來處理 `signal`。
在下面的第一個例子中,我們將看到 lambda 表達式如何漂亮地創建出一些插槽類型。這些插槽可以在調用 `connect` 的地方創建。第一個插槽在調用時簡單地輸出一個信息到 `std::cout` 。第二個插槽檢查 `signal` 傳入的字符串值。如果它等于 `"Signal"`, 則輸出一個信息;否則它輸出另一個信息。(這些例子確實有點做作,但這種表達式可以完成任何有用的計算)。該例子中創建的最后兩個插槽完成了本章前面的例子中的 `double_slot` 和 `plus_slot` 所做的工作。你會發現這個 lambda 版本更具可讀性。
```
#include <iostream>
#include <string>
#include <cassert>
#include "boost/signals.hpp"
#include "boost/lambda/lambda.hpp"
#include "boost/lambda/if.hpp"
int main() {
using namespace boost::lambda;
boost::signal<void (std::string)> sig;
sig.connect(var(std::cout)
<< "Something happened: " << _1 << '\n');
sig.connect(
if_(_1=="Signal") [
var(std::cout) << "Ok, I've got it\n"]
.else_[
std::cout << constant("Yeah, whatever\n")]);
sig("Signal");
sig("Another signal");
boost::signal<void (int&)> sig2;
sig2.connect(0,_1*=2); // 加倍
sig2.connect(1,_1+=3); // 加 3
int i=12;
sig2(i);
assert(i==27);
}
```
如果你還不熟悉C++(或其它)中的 lambda 表達式,不要為前面這段代碼看起來有點糊涂而著急,你可以先看看 Bind 和 Lambda 那兩章,然后再回到這個例子上來。如果你已經了解了 lambda 表達式,我可以肯定你一定會認為使用 lambda 表達式可以帶來簡潔的代碼;而且它避免了把代碼分割成多個小的函數對象。
現在讓我們來看看使用綁定器來創建插槽類型。插槽必須實現一個調用操作符,但不是所有的類都適合作為 插槽。另一方面,通常可以使用一些已有的類成員函數,用綁定器重新包裝它們以用作插槽。綁定器也有助于可讀性,它允許處理某個事件的函數(而不是函數對 象)具有一個有意義的名字。最后,有時同一個對象需要對不同的事件作出反應,每一個都有相同的插槽簽名,但是反應各有不同。因此,這種對象需要不同的成員 函數來為不同的事件所調用。在這些情形下,沒有一個調用操作符適用于連接到一個 `signal`. 因此,需要一個可配置的函數對象,而 Boost.Bind 正好提供了 (就象 Boost.Lambda 中的 `bind` 工具一樣) 需要的方法。
考慮一個 `signal`,它接受一個返回 `bool` 且接受一個類型 `double` 的參數的插槽類型。假設類 `some_class` 有一個成員函數 `some_function` ,它具有相符的簽名,你如何把 `some_class::some_function` 連接到 `signal` 呢?一個方法是給 `some_class` 增加一個調用操作符,而該調用操作符把調用前轉到 `some_function`. 這意味著要修改類的接口,而且它不好擴展。而綁定器可以做得更好。
```
#include <iostream>
#include "boost/signals.hpp"
#include "boost/bind.hpp"
class some_class {
public:
bool some_function(double d) {
return d>3.14;
}
bool another_function(double d) {
return d<0.0;
}
};
int main() {
boost::signal<bool (double)> sig0;
boost::signal<bool (double)> sig1;
some_class sc;
sig0.connect(
boost::bind(&some_class::some_function,&sc,_1));
sig1.connect(
boost::bind(&some_class::another_function,&sc,_1));
sig0(3.1);
sig1(-12.78);
}
```
綁定這種方法有一個有趣的副作用:它避免了不必要的 `some_class` 實例的拷貝。綁定器持有對 `some_class` 實例的指針,而 `signal` 復制的是綁定器。不幸的是,這種方法有一個潛在的生存期管理問題:如果 `sc` 被銷毀而后一個 `signal` 被調用,將導致未定義行為。這是因為綁定器將持有一個到 `sc` 的懸空指針。為了避免復制,我們必須負責保證插槽的生存期與(間接)引向它們的 `connection` 的存在一樣長。當然,這正是引用計數智能指針的功能,所以這個問題很容易解決。
在使用 Boost.Signals 時,象這樣使用綁定器是很常見的。無論你是使用 lambda 表達式來創建插槽,還是使用綁定器來把已有類改編為插槽類型使用,你都可以很快看到 Boost.Signals, Boost.Lambda, 與 Boost.Bind 相互配合的價值所在。它可以節省你的時間,并讓你的代碼更加美觀和簡潔。
- 序
- 前言
- Acknowledgments
- 關于作者
- 本書的組織結構
- Boost的介紹
- 字符串及文本處理
- 數 據結構, 容器, 迭代器, 和算法
- 函數對象及高級編程
- 泛 型編程與模板元編程
- 數學及數字處理
- 輸入/輸出
- 雜項
- Part I: 通用庫
- Library 1. Smart_ptr
- Smart_ptr庫如何改進你的程序?
- 何時我們需要智能指針?
- Smart_ptr如何適應標準庫?
- scoped_ptr
- scoped_array
- shared_ptr
- shared_array
- intrusive_ptr
- weak_ptr
- Smart_ptr總結
- Library 2. Conversion
- Conversion 庫如何改進你的程序?
- polymorphic_cast
- polymorphic_downcast
- numeric_cast
- lexical_cast
- Conversion 總結
- Library 3. Utility
- Utility 庫如何改進你的程序?
- BOOST_STATIC_ASSERT
- checked_delete
- noncopyable
- addressof
- enable_if
- Utility 總結
- Library 4. Operators
- Operators庫如何改進你的程序?
- Operators
- 用法
- Operators 總結
- Library 5. Regex
- Regex庫如何改進你的程序?
- Regex 如何適用于標準庫?
- Regex
- 用法
- Regex 總結
- Part II: 容器及數據結構
- Library 6. Any
- Any 庫如何改進你的程序?
- Any 如何適用于標準庫?
- Any
- 用法
- Any 總結
- Library 7. Variant
- Variant 庫如何改進你的程序?
- Variant 如何適用于標準庫?
- Variant
- 用法
- Variant 總結
- Library 8. Tuple
- Tuple 庫如何改進你的程序?
- Tuple 庫如何適用于標準庫?
- Tuple
- 用法
- Tuple 總結
- Part III: 函數對象與高級編程
- Library 9. Bind
- Bind 庫如何改進你的程序?
- Bind 如何適用于標準庫?
- Bind
- 用法
- Bind 總結
- Library 10. Lambda
- Lambda 庫如何改進你的程序?
- Lambda 如何適用于標準庫?
- Lambda
- 用法
- Lambda 總結
- Library 11. Function
- Function 庫如何改進你的程序?
- Function 如何適用于標準庫?
- Function
- 用 法
- Function 總結
- Library 12. Signals
- Signals 庫如何改進你的程序?
- Signals 如何適用于標準庫?
- Signals
- 用法
- Signals 總結