- 論壇徽章:
- 0
|
該是轉(zhuǎn)入到正題的時候了。前面啰啰嗦嗦扯了一大通,可能不懂的還是看不懂,有點興趣的人卻早就不耐煩了。十分抱歉,最近事情都撞到一塊了,有些安排不過來。不過慢一些也好,可以一邊敘述一邊思考,盡量考慮的周全一些,也歡迎大家一起來出主意。
首先需要給想達到的目標定下個框框。因為有前述很多的別人早已實現(xiàn)好的案例作為參考,所以對于一個signal-slot大致的模樣很容易清楚的,只需要模仿就可以了——這是中國特色的辦法。
讓我們看看使用boost.signal的一個完整的例子,這個例子基本上摘抄自前面的鏈接中,我把它組合到一個C/C++例子程序里,這樣有條件的可以自己試著運行一下。
運行這個例子需要以下環(huán)境:boost.signal庫及相關(guān)頭文件,GNU C++編譯器(我一般使用它來做例子測試,你也可以使用微軟的VC++6.0及以后版本,但不保證測試過)。因為boost是以源代碼方式發(fā)布的,需要自己編譯,這個比較耗時。網(wǎng)絡(luò)上有一些別人編譯好的版本可以拿來用,你可以針對自己的環(huán)境選擇。我實際使用的環(huán)境是cygwin(1)及隨之發(fā)布的庫及工具,國內(nèi)用戶可以從cygwin.cn網(wǎng)站安裝。
這個例子十分簡單,全部放到一個c++代碼文件里,編譯運行就可以,具體步驟就不說了。
在代碼頭部需要包含<boost/signals.hpp>頭文件,以及<iostream>,<string>等用于測試目的,然后加上申明"using namespace std;",以符合可能的習(xí)慣。
首先申明Signal,這個申明可以作為局部或者全局變量:
boost::signal<int(float, string)> sig;
模板中的內(nèi)容是一個函數(shù)類型,直接指明了signal發(fā)出時,調(diào)用接口是int(float, std::string)這樣一個函數(shù)。
演示簡便起見,在程序入口main函數(shù)里,我們直接發(fā)出它:
int main()
{
// ..., 之前這里我們需要預(yù)先建立signal-slot連接,后面添加
sig(3.1415926, "pi"); // 發(fā)出(emit)信號
return 0;
}
我們定義一個同樣類型的自由函數(shù)供調(diào)用,方便起見,你可以把它直接放到main函數(shù)的上面:
int Func(float val, string str)
{
// 顯示被調(diào)用到,我們打印調(diào)用信息
std::cout << "Func: " << "the value of " << str << " is " << val << endl;
return 0;
}
好了,可以建立signal-slot連接:
int main()
{
sig.connect(&Func); // 連接到Func函數(shù)
sig(3.1415926, "pi");
return 0;
}
可以編譯實際運行一下看看效果,鏈接要求預(yù)先編譯好的boost.signal,一般名字是libboost_signals-gcc-mt這樣。
上面這個例子實際上就相當(dāng)于:
int main()
{
Func(3.1415926, "pi");
return 0;
}
該沒人問這樣的問題吧——那干嘛還兜那么個圈子?
上面例子僅僅是相當(dāng)于原始函數(shù)指針的效果,讓我們繼續(xù)看看調(diào)用對象成員函數(shù)怎么辦。
先定義類,同樣擺到main函數(shù)的上面:
class Foo {
public:
int Func(float val, string str) {
std::cout << "Foo->Func: " << "the value of " << str << " is " << val << endl;
return 0;
}
};
然后添加類的實例及對實例方法的調(diào)用,這個時候需要要代碼頭部添加頭文件包含#include <boost/bind.hpp>
int main()
{
Foo obj; // 類Foo的實例
sig.connect(&Func); // 連接到Func函數(shù)
sig.connect(boost::bind(&Foo::Func, obj, _1, _2)); // 連接到obj的Func方法
sig(3.1415926, "pi");
return 0;
}
這里連接就復(fù)雜了一些,slot部分不再是一個簡單的函數(shù)指針,而是包括成員函數(shù)指針,實例以及參數(shù)部分的一個綁定。這是生成了一個新的函數(shù)供 signal調(diào)用,把成員函數(shù)和具體實例組合在一起,所以稱為函數(shù)組合或綁定;也因為相當(dāng)于實際調(diào)用函數(shù)中參數(shù)的增多,被稱為高階函數(shù),這也是來自于函數(shù)式編程領(lǐng)域里的標準稱呼。_1,_2這樣的東西被稱為占位符,為了給生成的函數(shù)對象的附加參數(shù),實際上是可以攜帶某種類型數(shù)據(jù)的對象。編譯運行看看效果是不是和想象一樣。
上述的用法有點復(fù)雜,但想法很直接,所以先舉例,下面讓我們看看如何調(diào)用函數(shù)對象。函數(shù)對象是一個特定的概念,該對象包含一個重載了()操作符的方法,這樣調(diào)用的時候就不需要使用函數(shù)名,而可以直接以類或?qū)ο竺,看上去就跟自由函?shù)一樣。舉例如下:
struct Bar {
int operator()(float val, string str) {
std::cout << "Bar: " << "the value of " << str << " is " << val << endl;
}
};
同樣把類定義放到main上面,我們增加main實體如下:
int main()
{
Foo obj; // 類Foo的實例
sig.connect(&Func); // 連接到Func函數(shù)
sig.connect(boost::bind(&Foo::Func, obj, _1, _2)); // 連接到obj的Func方法
sig.connect(Bar()); // 連接到某個Bar函數(shù)對象
sig(3.1415926, "pi");
return 0;
}
這次顯得非常簡潔,似乎比調(diào)用成員方法容易的多,但實際上是一回事。注意這里的Bar()本身不是調(diào)用,而只是通過缺省構(gòu)造函數(shù)生成一個臨時對象,然后當(dāng)signal發(fā)出的時候會調(diào)用其重載的()的操作符。它等價于:
sig.connect(boost::bind(&Bar::operator(), Bar(), _1, _2)); // 這個復(fù)雜的寫法是不是容易理解一些
還有這里暗含一個對象生存期的問題。無論是使用obj實例,還是臨時對象,boost::bind的實現(xiàn)是保持一份它們的拷貝,直到實際調(diào)用產(chǎn)生的那一刻。臨時對象因為都是一樣的不用考慮,如果是實例,那么在連接建立后的對實例的修改將不會影響到slot中持有的那個對象。如果你需要的不是這樣而是想要引用原有的實例對象,那么可以采用指針傳遞該實例,也就是:
sig.connect(boost::bind(&Foo::Func, &obj, _1, _2)); // 連接到指向obj實例指針/引用的Func方法
不要小看它們之間的差異,實際使用過程中,我們更多需要的可能是引用,而不是無數(shù)長相一致的初始對象。這種需求是如此廣泛,以至于boost中有一個非常小但用處很大的庫boost::ref,可以用來產(chǎn)生對象引用或者引用對象這個東西。它的實現(xiàn)如此簡單但思想經(jīng)典,所以我就摘抄如下:
template<class T>
class reference_wrapper
{
public:
typedef T type;
explicit reference_wrapper(T& t): t_(&t) {}
operator T& () const { return *t_; }
// ...
private:
T* t_;
};
template<class T>
inline reference_wrapper<T> const ref(T &t)
{
return reference_wrapper<T>(t);
}
然后,上面用取地址符的地方就可以換成:
sig.connect(boost::bind(&Foo::Func, boost::ref(obj), _1, _2)); // 這是不是更有c++的味道
上述的這些東西基本上等價于closuer,thunk之類的東西,這算不上什么,讓我們把問題變稍微復(fù)雜些。如果有人寫了個類似上述Func功能的函數(shù)Func1,但不小心參數(shù)弄反了(這是經(jīng)常的事):
int Func1(string str, float val)
{
std::cout << "Func1: " << "the value of " << str << " is " << val << endl;
return 0;
}
程序可能有其他地方也用到這個函數(shù),要改起來可能會有些混亂,沒關(guān)系,我們可以這么做:
sig.connect(boost::bind(&Func1, _2, _1));
類似的,通過boost::bind我們可以在signal參數(shù)的基礎(chǔ)上任意的增減調(diào)用參數(shù),達到匹配最終調(diào)用的目的。
進一步,boost::bind可以通過使用函數(shù)對象作為參數(shù),把函數(shù)和函數(shù)堆疊起來,這在函數(shù)式語言中稱作currying(2),和Haskell語言一樣這是為了紀念著名的邏輯學(xué)家Haskell Curry(3)。
比如,當(dāng)我們獲得pi值的時候,需要計算的是半徑為2的圓的面積,我們分別有上述的輸出函數(shù),和一個計算面積的函數(shù):
float Area(float r, float pi)
{
return r * r * pi;
}
然后在main代碼里增加:
sig.connect(boost::bind(&Func, boost::bind(Area, 2, _1), _2));
這里看出來boost::bind返回的就是一個函數(shù)對象。
無論是上述何種做法,都局限于函數(shù)的粒度上。為此我們不得不做些預(yù)先的設(shè)計工作,或者在遇到變化的時候,隨時準備添加一層類似于上述計算面積的過程。這本身當(dāng)然不是什么問題,問題在于當(dāng)我們說到層的時候,往往是指它們是需要獨立設(shè)計和思考的。我們有限的思維能力,讓我們局限于一個不算太長的代碼范圍內(nèi),這對像我這樣天資愚笨的人而言已經(jīng)是十分吃力了。而一旦出現(xiàn)了上述情況的話,我們就不得不打斷當(dāng)前的工作,跳轉(zhuǎn)到其他一個代碼空間里,做出某種變更,然后再返回到剛才被打斷的地方。這分散了我們的注意力,破壞了已經(jīng)存在于大腦中一個連續(xù)的邏輯思維過程,降低了效率和提高了出錯概率。
這個時候我們需要的是一個可以不用跳來跳去就可以完成工作的辦法,這就是lambda表達式所能帶來的好處。
什么是lambda表達式?數(shù)學(xué)意義上的定義就不在這里討論了(我水平也不夠)。程序設(shè)計語言中的lambda表達式也就是指由一系列運算符結(jié)合而成的表達式,它自身也可以同時成為一個新的表達式的部分。在命令式語言里,你可以把一個語句塊看成就是一個lambda表達式,雖然它不是嚴格意義上的一個表達式。和函數(shù)式語言中不同的是,在C語言里,你不能把語句塊直接拿來作為函數(shù)使用,C++也同樣如此。但是在Object C里有block的概念(4),可以把語句塊當(dāng)作可以調(diào)用的對象,在一些動態(tài)語言里也有類似的東西。我們用lambda表示式重寫上面的例子:
sig.connect([](float pi, string str) { Func(2 * 2 * pi, str); });
connect的參數(shù)就是一個lambda表達式,它和函數(shù)很像,只是沒有名字并且和函數(shù)體語句塊一起被定義。lambda表達式可以組合成小到一個語句的任意粒度,如果我們的調(diào)用對象粒度不大,并且不需要重用的話,這種表述方式無疑大大提高了編碼效率。
上述的例子是按照已經(jīng)被接納成為C++ 0x一部分的lambda表達式標準提議(5)寫就,相信不久將來就會出現(xiàn)大眾的代碼里。
關(guān)于lambda表達式,boost里面也早已有實現(xiàn),而且有兩個,一個是boost::lambda,還有一個是作為boost::spirit一部分的boost::phoenix(6)。兩者大同小異,前者比較早,已經(jīng)沒有什么變化,后者更新一些,支持更加全面一些。我們看看 boost::lambda如何來寫上面的例子:
sig.connect(boost::lambda::bind(&Func, boost::lambda::_1 * 2 * 2, boost::lambda::_2));
去掉boost::lambda這個長長的前導(dǎo)命名空間,是不是就很漂亮了?傮w上boost::lambda在表達一些簡單的運算是足可以勝任的,但在表示復(fù)雜的代碼流程和引用復(fù)雜的成員對象時有些繁瑣,因此使用上會受到一定的制約。
注:后面的部分例子只是為了展示一些高階能力,細節(jié)上沒有面面俱到,感興趣的可以自己去嘗試。
到目前為止我們演示了signal-slot主要部分:連接(connect)和發(fā)出(emit),使用中還有對連接的管理,比如至少可以斷開;以及對返回值的使用。這個例子中signal的函數(shù)原型具有int類型的返回值,可以在發(fā)出signal的地方獲得,也就是把signal當(dāng)作函數(shù)使用。在C++ 中這很正常,因為它實際上就是一個函數(shù)對象。不過這些都不關(guān)鍵,暫時就不一一討論了。
(1)http://cygwin.com
(2)http://en.wikipedia.org/wiki/Currying
(3)http://en.wikipedia.org/wiki/Haskell_Curry
(4)http://en.wikipedia.org/wiki/Blocks_%28C_language_extension%29
(5)http://www.open-std.org/JTC1/SC22/WG...2009/n2927.pdf
(6)http://spirit.sourceforge.net/dl_doc...tml/index.html
[ 本帖最后由 TiGEr.zZ 于 2009-10-14 21:14 編輯 ] |
|