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