boost 源碼剖析之:多重回調(diào)機制 signal( 下 )
劉未鵬
C++ 的羅浮宮 (http://blog.csdn.net/pongba)
在 本文的上篇 中,我們大刀闊斧的剖析了 signal 的架構(gòu)。不過還有很多精微之處沒有提到,特別是一個遺留問題還沒有解決:如果用戶注冊的是函數(shù)對象(仿函數(shù)), signal 又當如何處理呢?
下篇:高級篇
概述
在本文的上篇中,我們已經(jīng)分析了 signal 的總體架構(gòu)。至于本篇,我們則主要集中于將 函數(shù)對象 (即仿函數(shù))連接到 signal 的來龍去脈。 signal 庫的作者在這個方面下了很多功夫,甚至可以說,并不比構(gòu)建整個 signal 架構(gòu)的功夫下得少。
之所以為架構(gòu),其中必然隱藏著一些或重要或精妙的思想。
學(xué)過 STL 的人都知道,函數(shù)對象 [1] (function object) 是 STL 中的重要概念和基石之一。它使得一個對象可以像函數(shù)一樣被 “ 調(diào)用 ” ,而調(diào)用形式又是與函數(shù)一致的。這種一致性在泛型編程中乃是非常重要的,它意味著 “ 泛化 ” ,而這正是泛型世界所有一切的基礎(chǔ)。而函數(shù)對象又由于其攜帶的信息較之普通函數(shù)大為豐富,從而具有更為強大的能力。
所以 signal 簡直是 “ 不得不 ” 支持函數(shù)對象。然而函數(shù)對象又和普通函數(shù)不同:函數(shù)對象會析構(gòu)。問題在于:如果某個函數(shù)對象連接到 signal ,那么,該函數(shù)對象析構(gòu)時,連接是否應(yīng)該斷開呢?這個問題, signal 的設(shè)計者留給用戶來選擇:如果用戶覺得函數(shù)對象一旦析構(gòu),相應(yīng)的連接也應(yīng)該自動斷開,則可以將其函數(shù)對象派生自 boost::signals::trackable 類,意即該對象是 “ 可跟蹤 ” 的。反之則不用作此派生。這種跟蹤對象析構(gòu)的能力是很有用的,在某些情況下,用戶需要這種語義:例如,一個負責(zé)數(shù)據(jù)庫訪問及更新的函數(shù)對象,而該對象的生命期受某個管理器的管理,現(xiàn)在,將它連接到某個代表用戶界面變化的 signal ,那么,當該對象的生命期結(jié)束時,對應(yīng)的連接顯然應(yīng)該斷開 —— 因為該對象的析構(gòu)意味著對應(yīng)的數(shù)據(jù)庫不再需要更新了。
signal 庫支持跟蹤函數(shù)對象析構(gòu)的方式很簡單,只要將被跟蹤的函數(shù)對象派生自 boost::signals::trackable 類即可,不需要任何額外的步驟。解剖這個 trackable 類所隱藏的秘密正是本文的重點。
架構(gòu)
很顯然, trackable 類是整個問題的關(guān)鍵。將函數(shù)對象派生自該類,就好比為函數(shù)對象安上了一個 “ 跟蹤器 ” 。根據(jù) C++ 語言的規(guī)則,當某個對象析構(gòu)時,先析構(gòu)派生層次最高 (most derived) 的對象,再逐層往下析構(gòu)其子對象。這就意味著,函數(shù)對象的析構(gòu)最終將會導(dǎo)致其基類 trackable 子對象的析構(gòu),從而在后者的析構(gòu)函數(shù)中,得到斷開連接的機會。那么,哪些連接該斷開呢?換句話說,該斷開與哪些 signal 的連接呢?當然是該函數(shù)對象連接到的 signals 。而這些連接則全部保存在一個 list 里面。下面就是 trackable 的代碼:
class trackable {
typedef std::list<connection> connection_list;
typedef connection_list::iterator connection_iterator;
mutable connection_list connected_signals ;
...
}
connected_signals 是個 list ,其中保存的是該函數(shù)對象所連接到的 signals 。只不過是以 connection 的形式來表示的。這些 connection 都是 “ 控制性 ” [2] 的,一旦析構(gòu)則自動斷開連接。所以, trackable 析構(gòu)時根本不需要任何額外的動作,只要讓該 list 自行析構(gòu)就行了。
了解了這一點,就可以畫出可跟蹤的函數(shù)對象的基本結(jié)構(gòu),如 圖四 :
圖四
<shapetype id="_x0000_t75" stroked="f" filled="f" path="m@4@5l@4@11@9@11@9@5xe" o:preferrelative="t" o:spt="75" coordsize="21600,21600"><stroke joinstyle="miter"></stroke><formulas><f eqn="if lineDrawn pixelLineWidth 0"></f><f eqn="sum @0 1 0"></f><f eqn="sum 0 0 @1"></f><f eqn="prod @2 1 2"></f><f eqn="prod @3 21600 pixelWidth"></f><f eqn="prod @3 21600 pixelHeight"></f><f eqn="sum @0 0 1"></f><f eqn="prod @6 1 2"></f><f eqn="prod @7 21600 pixelWidth"></f><f eqn="sum @8 21600 0"></f><f eqn="prod @7 21600 pixelHeight"></f><f eqn="sum @10 21600 0"></f></formulas><path o:connecttype="rect" gradientshapeok="t" o:extrusionok="f"></path><lock aspectratio="t" v:ext="edit"></lock></shapetype><shape id="_x0000_i1025" style="WIDTH: 204.75pt; HEIGHT: 233.25pt" type="#_x0000_t75"><imagedata o:title="boost" src="file:///C:/DOCUME~1/pongba/LOCALS~1/Temp/msohtml1/01/clip_image001.gif"></imagedata></shape>
現(xiàn)在的問題是,每當該函數(shù)對象連接到一個 signal ,都會將相應(yīng) connection 的一個副本插入到其 trackable 子對象的 connected_signals 成員 ( 一個 list) 中去。然而,這個插入究竟發(fā)生在何時何地呢?
在本文的上篇中曾經(jīng)分析過連接的過程。對于函數(shù)對象,這個過程仍然是一樣。不過,當時略過了一些細節(jié),這些細節(jié)正是與函數(shù)對象相關(guān)的。現(xiàn)在一一道來:
如你所知,在將函數(shù) ( 對象 ) 連接到 signal 時,函數(shù) ( 對象 ) 會先被封裝成一個 slot 對象, slot 類的構(gòu)造函數(shù)如下:
slot(const F& f):slot_function(get_invocable_slot(f,tag_type(f)))
{
// 一個 visitor ,用于訪問 f 中的每個 trackable 子對象
bound_objects_visitor do_bind( bound_objects );
// 如果 f 為函數(shù)對象,則訪問 f 中的每一個 trackable 子對象
visit_each (do_bind,get_inspectable_slot [3] (f,tag_type(f)));
// 創(chuàng)建一個 connection ,表示 f 與該 slot 的連接,這是為了實現(xiàn) “delayed-connect”
create_connection();
}
bound_objects 是 slot 類的成員,其類型為 vector<const trackable*> ??上攵?,經(jīng)過第二行代碼 “visit_each(...)” 的調(diào)用,該 vector 中保存的將是指向 f 中的各個 trackable 子對象的指針。
“ 等等! ” 你敏銳的發(fā)現(xiàn)了一個問題: “ 前面不是說過,如果用戶要讓他的函數(shù)對象成為可跟蹤的,則將該函數(shù)對象派生自 trackable 對象嗎?那么,也就是說,如果 f 是個 “ 可跟蹤 ” 的函數(shù)對象,那么其中的 trackable 子對象當然只有一個(基類對象)!但為什么這里 bound_objects 的類型卻是一個 vector 呢?單單一個 trackable* 不就夠了么? ”
在分析這個問題之前,我們先來看一段例子代碼:
struct S1:boost::signals::trackable
{// 該對象是可跟蹤的!但并非一個函數(shù)對象
void test(){cout<<"test/n";}
};
...
boost::signal< void ()> sig;
{ // 一個局部作用域
S1 s1;
sig.connect( boost::bind(&S1::test,boost::ref(s1)) );
sig(); // 輸出 “test”
} // 結(jié)束該作用域 ,s1 在此析構(gòu),斷開連接
sig(); // 無輸出
boost::bind() 將 &S1::test [4] 的 “this” 參數(shù)綁定為 s1 ,從而生成一個 “void()” 型的仿函數(shù),每次調(diào)用該仿函數(shù)就相當于調(diào)用 s1.test() ,然而,這個仿函數(shù)本身并非可跟蹤的,不過,很顯然,這里的 s1 對象一旦析構(gòu),則該仿函數(shù)就失去了意義,從而應(yīng)該讓連接斷開。所以,我們應(yīng)該使 S1 類成為可跟蹤的(見 struct S1 的代碼)。
然而,這又能說明什么呢?仍然只有一個 trackable 子對象!但是,答案已經(jīng)很明顯了:既然 boost::bind 可以綁定一個參數(shù),難道不能綁定兩個參數(shù)?對于一個延遲調(diào)用的函數(shù)對象 [5] ,一旦其某個按引用語義傳遞的參數(shù)析構(gòu)了,該函數(shù)對象也就相應(yīng)失效了。所以,對于這種函數(shù)對象,其按引用傳遞的參數(shù)都應(yīng)該是可跟蹤的。在上例中, s1 就是一個按引用傳遞的參數(shù) [6] ,所以是可跟蹤的。所以,如果有多個這種參數(shù)綁定到一個仿函數(shù),就會有多個 trackable 對象,其中任意一個對象的析構(gòu)都會導(dǎo)致仿函數(shù)失效以及連接的斷開。
例如,假設(shè) C1,C2 類都是 trackable 的。并且函數(shù) test 的類型為 void(C1,C2) 。那么 boost::bind(&test,boost::ref(c1),boost::ref(c2)) 就會返回一個 void() 型的函數(shù)對象,其中 c1,c2 作為 test 的參數(shù)綁定到了該函數(shù)對象。這時候,如果 c1 或 c2 析構(gòu),這個函數(shù)對象也就失效了。如果先前該函數(shù)對象曾連接到某個 signal<void()> 型的 signal ,則連接應(yīng)該斷開。
問題在于,如何獲得綁定到某個函數(shù)對象的所有 trackale 子對象呢?
關(guān)鍵在于 visit_each 函數(shù) —— 我們回到 slot 的構(gòu)造函數(shù)(見上文列出的源代碼),其第二行代碼調(diào)用了 visit_each 函數(shù),該函數(shù)負責(zé)訪問 f 中的各個 trackable 子對象,并將它們的地址保存在 bound_objects 這個 vector 中。
至于 visit_each 是如何訪問 f 中的各個 trackable 子對象的,這并非本文的重點,我建議你自行參考源代碼。
slot 類的構(gòu)造函數(shù)最后調(diào)用了 create_connection 函數(shù),這個函數(shù)創(chuàng)建一個連接對象,表示函數(shù)對象和該 slot 的連接。 “ 咦?為什么和 slot 連接,函數(shù)對象不是和 signal 連接的嗎? ” 沒錯。但這個看似蛇足的舉動其實是為了實現(xiàn) “delayed connect” ,例如:
void delayed_connect(Functor* f)
{
// 構(gòu)造一個 slot ,但暫時不連接
slot_type slot(*f);
// 使用 f 做一些事情,在這個過程中 f 可能會被析構(gòu)掉
...
// 如果 f 已經(jīng)被析構(gòu)了,則 slot 變?yōu)? inactive 態(tài),則下面的連接什么事也不做
sig.connect(slot);
}
...
Functor* pf= new Functor();
delayed_connect(pf);
...
這里,如果在 slot 連接到 sig 之前, f“ 不幸 ” 析構(gòu)了,則連接不會生效,只是返回一個空連接。
為了達到這個目的, slot 類的構(gòu)造函數(shù)使用 create_connection 構(gòu)造一個連接,這個連接其實沒有實際意義,只是用于 “ 監(jiān)視 ” 函數(shù)對象是否析構(gòu)。如果函數(shù)對象析構(gòu)了,則該連接會變?yōu)? “ 斷開 ” 態(tài)。下面是 create_connection 的源代碼:
摘自 libs/signals/src/slot.cpp
void slot_base::create_connection()
{
basic_connection* con = new basic_connection();
con->signal = static_cast < void *>( this );
con->signal_data = 0;
con->signal_disconnect = &bound_object_destructed;
watch_bound_objects.reset(con);
...
}
這段代碼先 new 了一個連接,并將其三個成員設(shè)置妥當。由于該連接純粹僅作 “ 監(jiān)視 ” 該函數(shù)對象是否析構(gòu)之用,并非真的 “ 連接 ” 到 slot ,所以 signal_data 成員只需閑置為 0 ,而 signal_disconnect 所指的函數(shù) &bound_object_destructed 也只不過是個什么事也不做的空函數(shù)。關(guān)鍵是最后一行代碼: watch_bound_objects 乃是 slot 類的成員,類型是 connection ,這行代碼使其指向上面新建的 con 連接對象。注意,在后面省略掉的部分代碼中, 該連接的副本也被保存到待連接的函數(shù)對象的各個 trackable 子對象中 (前面已經(jīng)提到(參見圖四),這系保存在一個 list 中),這才真正使得 “ 監(jiān)視 ” 成為可能!因為這樣做了之后,一旦代連接的函數(shù)對象析構(gòu)了,將會導(dǎo)致 con 連接為 “ 斷開 ” 狀態(tài)。從而在 sig.connect(slot) 時可以通過查詢 slot 中的 watch_bound_objects 副本的連接狀態(tài)得知該 slot 是否有效,如果無效,則返回一個空的連接。這里, connection 巧妙的充當了一個 “ 監(jiān)視器 ” 的作用。
說到這里,你應(yīng)該也就明白了為什么 basic_connection 的 signal 和 signal_data 成員的類型為 void* 而不是 signal_base_impl* 和 slot_iterator* 了 —— 是的,因為函數(shù)對象不但連接到 signal ,還 “ 連接 ” 到 slot 。將這兩個成員類型設(shè)置為 void* 可以復(fù)用該類以使其充當 “ 監(jiān)視器 ” 的角色。 signal 庫的作者真可謂惜墨如金。
回到正題,我們接著考察如何將封裝了函數(shù)對象的 slot 連接到 signal 。這里,我建議你先回顧本文的上篇,因為這與將普通函數(shù)連接到 signal 有很大一部分相同之處,只不過多做了一些額外的工作。
同樣,可想而知的是,這個連接過程仍然是先將 slot 插入到 signal 中的 slot 管理器中去,并將 signal 的地址,插入后指向該 slot 的迭代器的地址,以及負責(zé)斷開連接的函數(shù)地址分別保存到表示本次連接的 basic_connection 對象的三個成員 [7] 中去。這時,故事幾乎已經(jīng)結(jié)束了一半 —— 用戶已經(jīng)可以通過該對象來控制相應(yīng)連接了。但是,注意,只是 “ 用戶 ” !對于函數(shù)對象來說,不但用戶能夠控制連接,函數(shù)對象也必須能夠 “ 控制 ” 連接,因為它析構(gòu)時必須能夠斷開連接,所以,我們還需要將該連接對象的副本保存到函數(shù)對象的各個 trackable 子對象中去:
摘自 libs/signals/src/signal_base.cpp
connection
signal_base_impl::
connect_slot(const any& slot,
const any& name,
const std::vector<const trackable*>& bound_objects)
{
... // 創(chuàng)建 basic_connection 對象并設(shè)置其成員
// 下面的 for 循環(huán)將該連接的副本保存到各個 trackable 子對象中
for (std::vector<const trackable*>::const_iterator i =
bound_objects.begin();
i != bound_objects.end();++i)
{
bound_object binding;
(*i)->signal_connected(slot_connection, binding );
con->bound_objects.push_back(binding);
}
...
}
在上面的代碼中, for 循環(huán)遍歷綁定到該函數(shù)對象的各個 trackable 子對象,并將該連接的副本 slot_connection 保存到其中。這樣,當某個 trackable 子對象析構(gòu)時,就會通過保存在其中的副本來斷開該連接,從而達到 “ 跟蹤 ” 的目的。
但是,這里還有個問題:這里實際的連接只有一個,但卻產(chǎn)生了多個副本,分別操縱在各個 trackable 子對象手中,如果用戶愿意,用戶還可以操縱一個或多個副本。但是,一旦該連接斷開 —— 不管是由于某個 trackable 子對象的析構(gòu)還是用戶手動斷開 —— 則保存在各個 trackable 子對象中的該連接的副本都應(yīng)該被刪除掉。不然既占空間又沒有任何意義,還會導(dǎo)致這樣的情況:只要其中有一個 trackable 對象還沒有析構(gòu),表示該連接的 basic_connection 對象就不會被 delete 掉。特別是當連接由用戶斷開時,每個未析構(gòu)的 trackable 對象中都會仍留有一個該連接對象的副本,直到 trackable 對象析構(gòu)時該副本才會被刪除。這就意味著,如果存在一個 “ 長命百歲 ” 的 trackable 函數(shù)對象,并在其生命期中頻繁被用戶連接到 signal 并頻繁斷開連接,那么,每次連接都會遺留一個連接副本在其 trackable 基類子對象中,這是個巨大的累贅。
那么,這個問題到底如何解決呢? basic_connection 仍然是問題的核心,既然用戶只能通過 connection 對象來控制連接,而 connection 對象實際上完全通過 basic_connection 來操縱連接,那么如何解決這個問題的責(zé)任當然落在 basic_connection 身上 —— 既然它知道哪個函數(shù)(對象)連接到哪個 signal 并在其 slot 管理器中的位置,那么,為什么不能讓它也知道 “ 該連接在各個 trackable 對象中的副本所在何處 ” 呢?
當然可以。答案就在于 basic_connection 的第四個成員 bound_objects ,其定義如下:
std::list<bound_object> bound_objects;
該成員正是用來記錄 “ 該連接在各個 trackable 對象中的副本所在何處 ” 的。它的類型是 std::list ,其中每一個 bound_object 型的對象都代表 “ 某一個連接副本所在之處 ” 。有了它,在斷開連接時,就可以依次刪除各個 trackable 對象中的副本。
那么,這個 bound_objects 又是何時被填充的呢?當然是在連接時,因為只有在連接時才知道有幾個 trackable 對象,并有機會將副本保存到它們內(nèi)部。我們回顧上文的 connect_slot 函數(shù)的代碼,其中有加底紋的部分剛才沒有分析,這正是與此相關(guān)的。為了清晰起見,我們將分析以源代碼注釋的形式寫出來:
//bound_object 對象保存的是連接副本在 trackable 對象中的位置
bound_object binding;
// 調(diào)用的是 trackable::signal_connected 函數(shù),該函數(shù)告訴 trackable 對象它已經(jīng)連接到了 signal ,并提供連接的副本(第一個參數(shù)),該函數(shù)會將該副本插入到 trackable 的成員 connected_signals (見篇首 trackable 類的代碼)中去。并將插入的位置反饋給 binding 對象(第二個參數(shù),按引用傳遞),這時候,通過 binding 就能夠?qū)⒃摳北緩? trackable 對象中刪除。
(*i)->signal_connected(slot_connection, binding );
// 將接受反饋后的 binding 對象保存到該連接的 bound_objects 成員中去,以便以后通過它來刪除連接的副本
con->bound_objects.push_back(binding);
要想完全搞清楚以上幾行代碼,我們還得來看看 bound_object 類的結(jié)構(gòu)以及 trackable::signal_connected 到底干了些什么?先來看看 bound_object 的結(jié)構(gòu):
摘自 boost/signals/connection.hpp
struct bound_object {
void * obj;
void * data;
void (*disconnect)( void *, void *);
}
發(fā)現(xiàn)什么特別的沒有?是的,它的結(jié)構(gòu)簡直就是 basic_connection 的翻版,只不過成員的名字不同了而已。 basic_connection 因為是控制連接的樞紐,所以其三個成員表現(xiàn)的是被連接的 slot 在 signal 中的位置。而 bound_object 表現(xiàn)的是 connection 副本在 trackable 對象中的位置。在介紹 bound_object 的三個成員之前,我們先來考察 trackable::signal_connected 函數(shù),因為這個函數(shù)同時也揭示了這三個成員的含義:
摘自 libs/signals/src/trackable.cpp
void trackable::signal_connected(connection c,
bound_object& binding )
{
// 將 connection 副本插入到 trackable 對象中的 connected_signals 中去, connected_signals 是個 std::list<connection> 型的容器,負責(zé)跟蹤該對象連接到了哪些 signal (見篇首的詳述)。
connection_iterator pos =
connected_signals.insert(connected_signals.end(), c);
// 將該 trackable 對象中保存的 connection 副本設(shè)置為 “ 控制性 ” 的,從而該副本析構(gòu)時才會自動斷開連接。
pos->set_controlling();
//obj 指針指向 trackable 對象,注意這里將 trackable* 轉(zhuǎn)型為 void* 以利于保存。
binding.obj = const_cast < void *>( reinterpret_cast < const void *>( this ));
//data 指向 connection 副本在 connected_signals 容器中的位置,注意這里的轉(zhuǎn)型
binding.data = reinterpret_cast < void *>( new connection_iterator(pos));
// 通過這個函數(shù)指針,可以將這個 connection 副本刪除: signal_disconnected 函數(shù)接受 obj 和 data 為參數(shù),將 connection 副本 erase 掉
binding.disconnect = & signal_disconnected ;
}
分析完了這段代碼, bound_object 類的三個成員的含義不言自明。注意,其最后一個成員是個函數(shù)指針,指向 trackable::signal_disconnected 函數(shù),這個函數(shù)負責(zé)將一個 connection 副本從某個 trackable 對象中刪除,其參數(shù)有二,正是 bound_object 的前兩個成員 obj 和 data ,它們合起來指明了一個 connection 副本的位置。
當這些副本在各個 trackable 子對象中都安置妥當后,連接就算完成了。我們再來看看連接具體是如何斷開的,對于函數(shù)對象,斷開它與某個 signal 的連接的過程大致如下:首先,與普通函數(shù)一樣,將函數(shù)對象從 signal 的 slot 管理器中 erase 掉,這個連接就算斷開了。其次就是只與函數(shù)對象相關(guān)的動作了:將保存在綁定到函數(shù)對象的各個 trackable 子對象中的 connection 副本清除掉。這就算完成了斷開 signal 與函數(shù)對象的連接的過程。當然,得看到代碼心里才踏實,下面就是:
void connection::disconnect()
{
if ( this ->connected()) {
shared_ptr<detail::basic_connection> local_con = con;
// 先將該函數(shù)指針保存下來
void (*signal_disconnect)( void *, void *) =
local_con->signal_disconnect;
// 然后再將該函數(shù)指針置為 0 ,表示該連接已斷開
local_con->signal_disconnect = 0;
// 斷開連接, signal_disconnect 函數(shù)指針指向 signal_base_impl::slot_disconnected 函數(shù),該函數(shù)在本文的上篇已作了詳細介紹
signal_disconnect(local_con->signal, local_con->signal_data);
// 清除保存在各個 trackable 子對象中的 connection 副本
typedef std::list<bound_object>::iterator iterator;
for (iterator i = local_con->bound_objects.begin();
i != local_con->bound_objects.end(); ++i) {
// 通過 bound_object 的第三個成員, disconnect 函數(shù)指針來清除該連接的每個副本
i->disconnect(i->obj, i->data);
}
}
}
前面已經(jīng)說過, bound_object 的第三個成員 disconnect 指向的函數(shù)為 trackable::signal_disconnected ,顧名思義, “signal” 已經(jīng) “disconnected” 了,該是清除那些多余的 connection 副本的時候了,所以,上面的最后一行代碼 “i->disconnect(...)” 就是調(diào)用該函數(shù)來做最后的清理工作的:
摘自 libs/signals/src/trackable.cpp
void trackable::signal_disconnected( void * obj, void * data)
{
// 將兩個參數(shù)轉(zhuǎn)型,還其本來面目
trackable* self = reinterpret_cast <trackable*>(obj);
connection_iterator* signal =
reinterpret_cast <connection_iterator*>(data);
if (!self->dying) {
// 將 connection 副本 erase 掉
self->connected_signals.erase(*signal);
}
delete signal;
}
這就是故事的全部。這個清理工作一完成,函數(shù)對象與 signal 就再無瓜葛,從此分道揚鑣?;剡^頭來再看看 signal 庫對函數(shù)對象所做的工作,可以發(fā)現(xiàn),其主要圍繞著 trackable 類的成員 connected_signals 和 basic_connection 的成員 bound_objects 而展開。這兩個一個負責(zé)保存 connection 的副本以作跟蹤之用,另一個則負責(zé)在斷開連接時清除 connection 的各個副本。
分析還屬其次,重要的是我們能夠從中汲取到一些納為己用的東西。關(guān)于 trackable 思想,不但可以用在 signal 中,在其它需要跟蹤對象析構(gòu)語義的場合也大可用上。這種架構(gòu)之最妙之處就在于用戶只要作一個簡單的派生,就獲得了完整的對象跟蹤能力,一切的一切都在背后嚴密的完成。
蛇足 & 再談?wù){(diào)用
還記得在本文的上篇分析的 “ 調(diào)用 ” 部分嗎?庫的作者藉由一個所謂的 “slot_call_iterator” 來完成遍歷 slot 管理器和調(diào)用 slot 的雙重任務(wù)。 slot_call_iterator 和 slot 管理器本身的 iterator 語義幾乎相同,只不過對前者解引用 (dereference ,即 “*iter”) 的背后其實調(diào)用了其指向的 slot 函數(shù),并且返回的是 slot 函數(shù)的返回值。這種特殊的語義使得 signal 可以將 slot_call_iterator 直接交給用戶制定的返回策略(如 max_value<> , min_value<> 等),一石二鳥。但是這里面有一個難以察覺的漏洞:一個設(shè)計得不好的算法可能會使迭代器在相同的位置上出現(xiàn)冗余的解引用,例如,一個設(shè)計的不好的 max_value<> 可能會像這樣:
T max = *first++;
for (; first != last; ++first)
max = ( *first > max)? *first : max;
這個算法本身的邏輯并沒有什么不妥,只不過注意到其中 *first 出現(xiàn)了兩次,這意味著什么?如果按照以前的說法,每一次解引用都意味著一次函數(shù)調(diào)用的話,那么同一個函數(shù)將被調(diào)用兩次。這可就不合邏輯了。 signal 必須保證每個注冊的函數(shù)有且僅有一次執(zhí)行的機會。
解決這個問題的任務(wù)落在庫的設(shè)計者身上,無論如何,一個普通用戶寫出上面的算法的確是件無可非議的事。一個明顯的解決方案是將函數(shù)的返回值緩存起來,第二次或第 N 次在同一位置解引用時只是從緩存中取值并返回。 signal 庫的設(shè)計者正是采用的這種方法,只不過, slot_call_iterator 將緩存的返回值交給一個 shared_ptr 來掌管。這是因為,用戶可能會拷貝迭代器,以暫時保存區(qū)間中的某個位置信息,在拷貝迭代器時,如果緩存中已經(jīng)有返回值,即函數(shù)已經(jīng)調(diào)用過了,則新的迭代器也因該引用那個緩存。并且,當最后一個引用該緩存的迭代器消失時,就是該緩存被釋放之時,這正是 shared_ptr 用武之地。具體的實現(xiàn)代碼請你自行參考 boost/signals/detail/slot_call_iterator.hpp 。
值得注意的是, slot_call_iterator 符合 “single pass” (單向遍歷) concept 。對于這種類型的迭代器只能進行兩種操作:遞增和比較。這就防止了用戶寫出不規(guī)矩的返回策略 —— 例如,二分查找(它要求一個隨機迭代器)。如果用戶硬要犯規(guī),就會得到一個編譯錯誤。
由此可見,設(shè)計一個完備的庫不但需要技術(shù),還要無比的細心。
結(jié)語
相對于 C++ 精致的泛型技術(shù)的應(yīng)用來說,其背后隱藏的思想更為重要。在 signal 庫中,泛型技術(shù)的應(yīng)用其實也不可不謂淋漓盡致,但是語言只是工具,重要的是解決問題的思想。從這篇文章可以看出,作者為了構(gòu)建一個功能完備,健壯,某些特性可定制的 signal 架構(gòu)付出了多少努力。雖然某些地方看似簡單,如 connection 對象,但是都是經(jīng)過反復(fù)揣摩,時間檢驗后作出的設(shè)計抉擇。而對于函數(shù)對象,更是以一個 trackable 基類就實現(xiàn)了完備的跟蹤能力。以一個函數(shù)對象來定制返回策略則是符合 policy-based 設(shè)計的精髓。另外還有一些細致入微的設(shè)計細節(jié),本篇并沒有一一分析,一是為了讓文章更緊湊,二是篇幅 —— 只講主要脈絡(luò)文章尚已如此,再加上各個細節(jié)則更是 “ 了得 ” 了,干脆留給你自行理解,你將 boost 的源代碼和本文列出的相應(yīng)部分比較后或會發(fā)現(xiàn)一些不同之處,那些就是我故意省略掉的細節(jié)所在了。對于細節(jié)有興趣的不妨自己分析分析。
目錄 ( 展開 《 boost 源碼剖析》系列 文章 )
[1] 函數(shù)對象即重載了 operator() 操作符的對象,故而可以以與函數(shù)調(diào)用一致的語法形式來 “ 調(diào)用 ” 。又稱為 functor ,中文譯為 “ 仿函數(shù) ” 。
[2] “ 控制性 ” 是指該 connection 析構(gòu)時會順便將該連接斷開。反之則不然。關(guān)于 “ 控制性 ” 和 “ 非控制性 ” 的 connection 的詳細討論見本文的上篇。
[3] get_inspectable_slot() 當且僅當 f 是個 reference_wrapper 時,返回 f.get()—— 即其中封裝的真實的函數(shù)(對象)。其它時候,該函數(shù)調(diào)用等同于 f 。關(guān)于 reference_wrapper 的詳細介紹見 boost 的官方文檔。
[4] &S1::test 為指向成員函數(shù)的指針。其調(diào)用形式為 (this_ptr->*mem_fun_ptr)() 或 (this_ref.*mem_fun_ptr)() ,而從一般語義上說,其調(diào)用形式為 mem_fun_ptr(this_ref) 或 mem_fun_ptr(this_ptr) 。所以, boost::bind 可以將其“第一個”參數(shù)綁定為 s1 對象。
[5] command 模式,其中封裝的 command 對象就是一個延遲調(diào)用的函數(shù)對象,它暫時保存某函數(shù)及其調(diào)用的各個參數(shù),并在恰當?shù)臅r候調(diào)用該函數(shù)。
[6] boost::ref(s1) 生成一個 boost::reference_wrapper<S1>(s1) 對象,其語義與 “ 裸引用 ” 幾乎一樣,只不過具有拷貝構(gòu)造,以及賦值語義,這有點像 java 里面的對象引用。具體介紹見 boost 的官方文檔。
[7] signal 成員指向連接到的 signal , signal_data 成員指向該函數(shù)在 signal 中保存的位置(一般為迭代器),而 signal_disconnect 則是個函數(shù)指針,負責(zé)斷開連接,將前兩個成員作為參數(shù)傳給它就可以斷開連接。
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯(lián)系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
