boost源码剖析之:Tuple Types(rev#2)
boost源碼剖析之:Tuple Types(rev#2)
?
劉未鵬(pongba)
C++的羅浮宮(http://blog.csdn.net/pongba)
?
Note:?并非新作,04年曾放在blog上,后來刪掉了,不過網上到處有轉載。這是修改之后的版本。
?
動機[1]
假設你有這樣一個函數:它接受兩個整型數據并返回它們整除的結果,像這樣:
?
int?DevideInts(int?n,int?d)
{
???return?n/d;
}
?
但是我們可能需要更多信息,比如,余數。函數的返回值已被占用,我們可以為函數加一個參數:
?
int?DevideInts(int?n,int?d,int& Remainder)?
{
????Remainer=n%d;?
????return?n/d;
}
?
但是這樣的函數形式未免有些拖沓丑陋。我們可以使用std::pair<>來定義函數的返回值類型(顧名思義,std::pair<>可以將兩個值湊成一對),像這樣:
?
std::pair<int,int> DevideInts(int?n,int?d)
{
????return?std::pair<int,int>(n/d,n%d);
}
?
這是個可行的方案。簡潔,優雅。
然而,這個方案只能提供兩個返回值的捆綁,如果現在需要返回三個int呢?唔...你可能很快想到這樣組織代碼:
?
std::pair<int,std::pair<int,int> > someFunc();
?
的確,這也能夠工作,但是畢竟不夠精致!如果返回值再增加,代碼將會愈發丑陋不堪。另一個可行的方案是自己定義一個結構來保存三個乃至更多值,然而隨著不同函數的需要你可能需要定義各種不同的類似這樣的結構,這太費神了。
所以,我們需要的是一個高度可復用的,能夠用來保存任意型別的任意多個變量的類——Tuple Types(Tuple的意思是“元組,數組”)。正如你所想象的,泛型正是提供代碼復用的最佳手段,它將型別信息抽象出來,直到用戶真正使用那些代碼時,型別信息才得以落實(所謂“具現化”)。
Boost庫提供了所謂的Tuple Types,它沒有std::pair的限制,于是你可以寫:
?
//tuple<>目前能夠支持多達10個模板參數
boost::tuple<int,int,int> someFunc();
?
事實上tuple能夠提供的不止這個,tuple對IO流的支持能夠允許你寫這樣的代碼:
?
tuple<int,int,int> t(8,9,10);
std::cout<<t;??//輸出(8??9??10)
?
tuple甚至還支持類似的流控制,像這樣:
?
std::cout << tuples::set_open(‘[‘)
<< tuples::set_close(‘]’)
<< tuples::set_delimiter(‘,’)
<< t;
//輸出[8,9,10]
?
好了,你可能已經不耐煩了,畢竟,以上的內容非常淺顯。然而我必須要告訴你這些,因為你首先得知道tuple的設計目的才能夠去了解它。好在這個枯燥的過程已經結束了。深吸一口氣,我們去看一看tuple的設計細節和最本質的東西——源代碼。
?
設計目標
首先,了解tuple的設計目標十分重要。上面所講的只是一個總的設計目標。下面兩個細節設計目標才是真正需要和體現技術的地方(并且考慮它們如何能夠最佳實現是非常有趣的事情,當然,在你的種種考慮之后,你得承認,Boost庫的設計無疑是最精致和高效的),容我向你闡述它們:
?
tuple中的數據成員的個數應該具有某種動態特性。具體的說就是如果你像這樣具現化tuple: tuple<int,int> t。則t某種程度上應該只需要sizeof(int)*2大小的內存來存放它的數值,不應該有多余的內存分配。而如果是tuple<int,int,int> t;則sizeof(t)某種程度上應該為sizeof(int)*3。當然,你可以利用模板偏特化來實現這一點——為提供不同模板參數個數的tuple實現不同的偏特化版本(也就是說,對提供了N個模板參數的tuple準備的偏特化版本中具有N個數據成員)——但是,想想這樣做的代碼數量吧!你也可以使用動態分配底層容器的策略,然而那會帶來額外的負擔,顯然不如將數據直接放在tuple對象里,況且底層容器又該如何設計呢?事實上,boost::tuple并沒有使用以上任何一種手法,它使用了一種類似Loki庫[2]里的TypeList設施的手法來定義它的底層容器,這種精致的手法利用了某種遞歸的概念,極大的減少了代碼量。后面我會為你介紹它。
tuple?必須提供某種途徑以獲取它內部保存的數值。類似的,通過某種編譯期的遞歸,Boost極其巧妙地達到了這個目標。遺憾的是,由于技術上的原因,當你需要獲取第N個數據時,你所提供的N必須是編譯期可計算出的常量。這也體現出C++泛型缺少一些運行期的特性——是的,C++泛型幾乎完全是編譯期的。
?
其實,雖然上面我只為你描述了兩個設計目標,但是實作時仍會有各種小問題出現。下面的源碼剖析中我會一一為你解惑。
好吧,在你發出抱怨聲之前,我還是快點轉入我們的主題:
?
boost::tuple源碼剖析
boost::tuple的實現有許多精妙之處,真是千頭萬緒不知從何說起。還是從一個最簡單的應用展開吧:
?
//請記住它,后面我們將一直圍繞這個例子
boost::tuple<int,long,bool> myTuple(10,10,true);
?
以上簡單的代碼的背后其實發生了很多事,了解了這些事你幾乎就了解了關于tuple的一大半奧秘。首先我們肯定想知道tuple的聲明是什么樣子的,在boost/tuple/detail/tuple_basic.hpp中聲明了它,其中也包括tuple幾乎所有的實現:
?
template?<?class?T0 = null_type,?class?T1 = null_type,?class?T2 = null_type,
class?T3 = null_type,?class?T4 = null_type,?class?T5 = null_type,
class?T6 = null_type,?class?T7 = null_type,?class?T8 = null_type,
class?T9 = null_type > //?null_type是個空類
class?tuple;??//?注意這個聲明的所有模板參數都有缺省值
?
下面是boost::tuple的定義(也摘自boost/tuple/detail/tuple_basic.hpp):
?
????template?<class?T0,?class?T1,?class?T2,?class?T3,?class?T4,
???????????????class?T5,?class?T6,?class?T7,?class?T8,?class?T9>
????class?tuple :
??????public?detail::map_tuple_to_cons<T0, T1, T2, T3, T4,
T5, T6, T7, T8, T9>::type
{
// tuple的定義體十分簡單,其中是若干構造函數(將參數轉交給基類)和模板賦值操作符
??…
}; //?為了凸顯重點,以下先講tuple的基類
?
其實tuple本身的定義并無奧秘和技巧可言,所有秘密都藏在它的基類里面,tuple只是將參數轉交給基類處理。下面我為你剖析它的基類:
?
基類大廈的構建
?
構建大廈的腳手架——map_tuple_to_cons<>
在我們給出的極其簡單的應用代碼中:tuple<int,long,bool> myTuple(10,10,true);其實相當于:
?
tuple<int,long,bool,
null_type,null_type,null_type,null_type,
null_type,null_type,null_type
>?myTuple(10,10,true);
?
這是因為tuple的定義中所有模板參數都有缺省值,所以你沒有給出值的模板參數自然會被編譯器認為是缺省值null_type。這樣T0,T1,...,T9分別是int,long,bool,null_type,.....null_type。你發現基類的表現方式非常怪異——是一個map_tuple_to_cons<>中的內嵌型別::type。很自然,你該知道map_tuple_to_const<>的定義,下面就是:
?
????template?<class?T0,?class?T1,?class?T2,?class?T3,?class?T4,
???????????????class?T5,?class?T6,?class?T7,?class?T8,?class?T9>
????struct?map_tuple_to_cons
????{
????//?cons<>是數據的容器,也是所有奧秘所在
????1?typedef?cons<
T0, //第一個參數T0被孤立出來
?????????typename?map_tuple_to_cons< //剩下的模板參數后跟一個null_type進入下一輪
T1, T2, T3, T4, T5,T6, T7, T8, T9,?null_type
>::type
???????> type;
};
?
以及它的一個特化版本:
?
template?<>??//這個特化版本是終止某種遞歸式的自包含定義的關鍵,后面你會明白
struct?map_tuple_to_cons<null_type, null_type, null_type, null_type,
null_type, null_type, null_type, null_type,
null_type, null_type>
???{
??????2?typedef?null_type type;
};
?
就這么簡單。但是它的機理卻并非那么明顯:上面已經知道T0,T1,...,T9被推導為int,long,bool,null_type,...,null_type(其中省略號表示null_type,下同)。因此tuple的基類:
?
detail::map_tuple_to_cons<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9>::type
?
被推導為
?
map_tuple_to_cons<int,long,bool,null_type,...,null_type>::type
?
而根據map_tuple_to_cons的定義1,這其實就是:
?
cons<?int,
typename?map_tuple_to_cons<long,bool,null_type,...,null_type>::type
>
?
其中的
typename?map_tuple_to_cons<long,bool,null_type,...,null_type>::type
再一次涉及1處的typedef,因而它被推導為
?
cons<long,typename?map_tuple_to_cons<bool,null_type,...,null_type>::type>
?
所以現在看看基類的定義的形式被推導成為的樣子吧:
?
cons<int,
cons<long,
typename?map_tuple_to_cons<bool,null_type,...,null_type>::type
>?
>?
?
看出端倪了嗎?其中
typename?map_tuple_to_cons<bool,null_type,...,null_type>::type
仍然使用1處的typedef,從而為
cons<bool,
typename?map_tuple_to_cons<null_type,null_type,...,null_type>::type
>?
?
現在,我們推導至這樣一種遞歸嵌套的模式:
?
cons<int,
cons<long,
cons<bool,
typename?map_tuple_to_cons<null_type,...,null_type>::type
>?
>?
>?
?
好了,該是結束這場游戲的時候了,你應該看出來了,map_tuple_to_cons<>準備了一個特化版本來作為這場類似繞口令的遞歸式包含的休止符。所以,以上的定義再作最后一重推導,使用2處的typedef,將
typename?map_tuple_to_cons<null_type,...,null_type>::type
推導為null_type,得到最終的形式:
?
cons<int,cons<long,cons<bool,null_type> > >?
//?這實際上只是為int,long,bool各分配一份空間
?
這就是tuple<int,long,bool>的基類!!現在,你應該可以類似地推導出:如果tuple的形式為tuple<int,long,bool,double>,則其基類為:
?
cons<int,cons<long,cons<bool,cons<double,null_type> > > >。
?
這樣,隨著你給出的模板參數個數的不同(意味著你要求保存的數據的個數不同,tuple的基類竟能夠呈現出某種動態的特性(用戶提供的模板參數個數的變化(反映用戶需要保存的數據的個數)導致cons<>容器的嵌套層數的變化,進而導致tuple的底層內存的分配量也作相應變化)。
map_tuple_to_cons<>以一種遞歸的方式不斷將它的第一個模板參數割裂出來,并使tuple的基類呈現像這樣的形式:
?
cons<T0,cons<T1,cons<T2,cons<T3,... ... > > > >
?
這種遞歸當map_tuple_to_cons<>的模板參數都為null_type時才恰好停止,由于map_tuple_to_cons<>不斷將第一個模板參數取出,并將剩余的參數在尾部添一個null_type再傳遞下去。所以當用戶給出的模板參數全部被分離出來時,map_tuple_to_cons<>所接受的參數就全部都是null_type了,于是使用其特化版本,其中將內嵌型別type typedef為null_type。從而結束這場遞歸。
map_tuple_to_cons<>其實在tuple的定義中充當了十分重要的角色,如果沒有它的介入,難道還有更簡潔美妙的方式來達到這個目的嗎?
?
構建大廈的磚石——cons<>
現在,你一定非常想看一看cons<>的定義,下面就是:
?
template?<class?HT,?class?TT>
???struct?cons {
?????typedef?HT head_type; //?這是個用戶提供的型別
?????typedef?TT tail_type;???//?這通常是個cons<>的具現體
?????????????????????????????//?以上兩個typedef很重要,并非可有可無
?????typedef
???????typename?detail::wrap_non_storeable_type<head_type>::type
stored_head_type;
3???stored_head_type?head; //?這是其中第一個數據成員
4???tail_type?tail;????????????//?第二個數據成員
?...????????????????????????//?其成員函數將在后面解釋,此處先略去
};
// cons<>還有一個偏特化版本:
template?<class?HT>
???struct?cons<HT, null_type> {
typedef?HT head_type;
?????typedef?null_type tail_type;
?????typedef?cons<HT, null_type> self_type;
?????typedef?typename
???????detail::wrap_non_storeable_type<head_type>::type stored_head_type;
?
stored_head_type?head;
//?注意,不像上面的主模板,這里沒有tail成員
?????... //?成員函數將在后面解釋
};
?
根據cons<>的定義顯示它有兩個數據成員:3,4兩處描述了它們,對于第一個數據成員的型別stored_head_type,往它上面看一行,它被typedef為:
?
detail::wrap_non_storeable_type<head_type>::type
//?而head_type又被typedef為HT
?
這又是個什么玩意?其實它只是用來偵測你是否使用了void型別和函數類型(所謂函數型別就是像void(int,int)這樣的型別,它表示接受兩個int型參數返回void的函數的型別,注意,它不同于函數指針型別,后者形式為void(*)(int,int),void(*f)(int,int)定義了一個函數指針f,而void f(int,int)無疑是聲明了一個函數f)來具現化tuple,如果是的,那它得采取特殊手段,因為這兩種型別不能像int那樣定義它們的變量(你見過void val;這樣定義val變量的嗎)。“但是”你急忙補充“這本就應該不能通過編譯呀?”是的,寫void?val;這樣的語句不應該通過編譯,寫tuple<void> myTuple;這樣的語句也應該不能通過編譯。但是,typedef?void?VoidType;這樣的typedef卻應該是能夠通過編譯的,所以typedef?tuple<void> voidTupleType;這樣的typedef也該能夠通過編譯。然而如果在cons<>里單純地寫上:
?
HT head;??//如果HT為void則這將導致編譯錯誤
?
這個成員,則tuple<void>這樣的具現化肯定會惹惱編譯器(因為它將會發覺cons<>里試圖定義一個void型的變量)。
所以,對于這種情況,boost使用了wrap_non_storeable_type<>,它的定義是這樣的:
?
template?<class?T>
struct?wrap_non_storeable_type {
typedef?typename?IF<?????????????// IF<>相當于編譯期的if...then...else
???????::boost::is_function<T>::value, //?如果為函數類型則特殊處理
non_storeable_type<T>, T??????//?如果不是函數類型則type就是T
?????>::RET type;
};
?
以及其特化版本:
?
???template?<>
struct?wrap_non_storeable_type<void> { //?如果為void型也特殊處理
?????typedef?non_storeable_type<void> type;?
???};
?
里面的non_storeable_type<>其實是函數型別和void型別的外覆類,以使得它們可以合法的作為數據成員被定義。你不能將void?dataMember;作為數據成員,但你可以將non_storeable_type<void> wrappedData;作為成員。你不能將void?f(int,int)作為數據成員,但你可以將non_storeable_type<void(int,int)> wrapperdData;作為成員。但是,雖然這樣能夠使tuple<void>這樣的型別得以具現出來,然而你仍然不能擁有它們的對象,像tuple<void> myTuple;這樣的代碼仍然無法通過編譯,原因是non_storeable_type<>模板類是這樣定義的:
?
???template?<class?T>
class?non_storeable_type {
??????non_storeable_type();??//?僅有私有的構造函數,意味著不能擁有該類的對象實體
???};
?
一旦你以tuple<void>為型別定義了一個變量,則該類內部的成員須被初始化,而non_storeable_type<>的構造函數為私有,所以初始化失敗,產生編譯錯誤。
所有這些正符合void及函數型別的特性——能夠被typedef,卻不能擁有數據對象實體。(boost的實現者可真夠細心的)
好了,從細節中回過神來。我們通常顯然不會用void和函數型別來具現化tuple。所以,通常,cons<>內部的兩個數據成員的型別通常其實就是:
?
?????HT head;
?????TT tail;
?
現在回顧我們的示例代碼:tuple<int,long,bool> myTuple;tuple<int,long,bool>的基類為:
?
????cons<int,cons<long,cons<bool,null_type> > >
?
所以,最外層的cons<>的模板參數被推導為:
?
typename?HT=int,typename?TT=?cons<long,cons<bool,null_type> >
?
這樣,tuple<int,long,bool>的基類cons<int,cons<long,cons<bool,null_type> > >其實只擁有兩個成員:
?
int?head;
cons<long,cons<bool,null_type> > tail; //?注意這又是一個cons<>對象
?
tail成員又是cons<>的一個對象,不同的是tail的型別不同了——具現化cons<>的模板參數不同。可想而知,tail內部包含兩個成員:
?
long?head;
cons<bool,null_type> tail;
?
值得注意的是,第二個tail的型別匹配的是cons<>的偏特化版本,其中只有一個數據成員:
?
bool?head;
?
所以整個基類的內存布局其實就是cons<>的三重嵌套。三個head數據成員就是需要分配內存的主體。如果將這種布局擴展,大概就像這樣:
?
?
這種布局正像一種玩具——開始是一個盒子,揭開盒子其內部又是個更小的盒子,再揭,還是盒子...
現在,基類的內存布局已經展現在你面前。這一切其實就是由那個魔棒般的map_tuple_to_cons<>所造就的,它建造了這種嵌套式的結構。這樣構建的好處就是嵌套的重數可以由用戶給出的模板參數個數來控制。前者體現了底層內存的占用量(如果重數為N重,則只有N個head占用內存),后者體現用戶的需求量。這正是一種“按需分配”。
在基類的大廈構架完畢后,問題自然是,如何將材料填入這幢蜂窩般的大廈。這得從tuple的構造函數入手,下面我就帶你作一次跟蹤。
?
初始化的全過程
然而在跟蹤之前我們須了解tuple的構造函數,因為所有初始化參數由此進入:
?
???template?<class?T0,?class?T1,?class?T2,?class?T3,?class?T4,
??????????????class?T5,?class?T6,?class?T7,?class?T8,?class?T9>
???class?tuple :
????public?detail::map_tuple_to_cons<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9>::type
???{
???public:
?????typedef?typename
???????detail::map_tuple_to_cons<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9>::type
inherited; //?基類
?????typedef?typename?inherited::head_type head_type;
//?基類的head_type(通常即T0,見cons<>的定義)
?????typedef?typename?inherited::tail_type tail_type;
//?基類的tail_type(一般仍為一個cons<>)
????
?????//下面有十一個構造函數,我只給出兩個,其它類同,只不過參數個數增加而已
????tuple() {} //?這里也調用基類的默認構造函數
????
// access_traits<>的定義后面解釋
????tuple(typename?access_traits<T0>::parameter_type t0)
?????: inherited(t0, detail::cnull(),????????????// cnull函數返回null_type()對象
detail::cnull(), detail::cnull(), //?可將detail::cnull()看作null_type()
detail::cnull(), detail::cnull(),
detail::cnull(), detail::cnull(),
detail::cnull(), detail::cnull())
{ }
????tuple(typename?access_traits<T0>::parameter_type?t0,
????????typename?access_traits<T1>::parameter_type?t1) //增加了一個參數t1
????: inherited(t0, t1, detail::cnull(), detail::cnull(),
????????????????detail::cnull(), detail::cnull(), detail::cnull(),
????????????????detail::cnull(), detail::cnull(), detail::cnull())
{ }
????...
??};
?
其中構造函數的參數型別以access_traits<>來表現是有原因的,它的定義如下:
?
???template?<class?T>
struct?access_traits {
?????typedef?const?T& const_type;
?????typedef?T& non_const_type;
?????typedef?const?typename?boost::remove_cv<T>::type& parameter_type;
};
?
parameter_type正是在tuple構造函數中被用作參數型別的。先由remove_cv將T型別可能具有的const或volatile修飾符去掉,然后再加上const修飾符以及表示引用的符號&,就是parameter_type。舉個例子,如果我們給T0的模板參數為int,則typenameaccess_traits<T0>::parameter_type就是const int&。為什么要作這么麻煩的舉動,就是因為你可能會將常量或臨時對象作為參數傳遞給構造函數,而C++標準不允許它們綁定到非const引用。為什么要用引用型別作參數型別?自然是為了效率著想。
當然,如果你想直接在tuple內保存引用也可以,如果你將T0賦為int&,這時候parameter_type并不會被推導為int&&(引用的引用是非法的),原因是access_traits為此準備了一個偏特化版本,如下:
?
???template?<class?T>?struct?access_traits<T&> {
??????typedef?T& const_type;
??????typedef?T& non_const_type;
??????typedef?T& parameter_type;??
???};
?
如果T0本身是個引用,則對parameter_type的推導將使用該偏特化版本。不過你該會發現這個偏特化版本中的parameter_type被定義為T&而非const T&,這是因為,如果你的意圖是在tuple中保存一個int&,則出現在構造函數中的參數的型別就該是int&而非const int&,因為不能用const int&型別的參數來初始化int&型別的成員。
好吧,現在回到我們的例子,我們具現化tuple為tuple<int,long,bool>則該具現體的構造函數應該是這樣子:
?
A??tuple(){}
B??tuple(const?int& t0) : inherited(t0, detail::cnull(),...,detail::cnull()){}
C??tuple(const?int& t0,const?long& t1)
: inherited(t0,t1,detail::cnull(),...,detail::cnull())
{ }
D??tuple(const?int& t0,const?long& t1,const?bool& t2)
??: inherited(t0,t1,t2,detail::cnull(),...,detail::cnull())
{ }
E??tuple(const?int& t0,const?long& t1,const?bool& t2,const?null_type&?t3)
?:?inherited(t0,t1,t2,detail::cnull(),..)
{ } //?這不可用
...??//?其他構造函數以此類推
?
這樣一堆構造函數,有那些可用呢。事實上,你可以有以下幾種初始化方法:
?
tuple<int,long,bool> MyTuple; //ok,所有成員默認初始化,調用A
tuple<int,long,bool> MyTuple(10); //ok,第一個成員賦值為10,其它兩個默認初始化,調用B
tuple<int,long,bool> MyTuple(10,10);//ok,給第一第二個成員賦值,調用C
tuple<int,long,bool> MyTuple(10,10,true);//ok,給三個成員都賦初始值,調用D
?
在tuple的構造函數背后發生了什么事情呢?當然是其基類的構造函數被調用,于是我們跟蹤到cons<>的構造函數,它的代碼是這樣的:
?
???template?<class?HT,?class?TT>
struct?cons {
...
?????template?<class?T1,?class?T2,?class?T3,?class?T4,?class?T5,
????????????????class?T6,?class?T7,?class?T8,?class?T9,?class?T10>
?????cons( T1& t1, T2& t2, T3& t3, T4& t4, T5& t5,
???????????T6& t6, T7& t7, T8& t8, T9& t9, T10& t10 )
??????????:?head?(t1),?tail?(t2, t3, t4, t5, t6, t7, t8, t9, t10, detail::cnull())
??{ }
...
};
?
現在假設我們這樣初始化一個tuple:
?
tuple<int,long,bool> MyTuple(10,11,true);
?
則調用tuple的D構造函數被喚起,并將三個參數傳給其基類,第一重cons<>將其head賦為10,再將剩下的參數悉數傳給其tail,后者又是個cons<>,它將它的head賦為11(注意,這時它接受到的第一個參數是11),然后將僅剩的true加上后面的九個null_type一股腦兒傳給它的tail—cons<bool,null_type>(最內層的cons<>)。cons<HT,null_type>這個偏特化版本的構造函數是獨特的,因為它只有head沒有tail成員,所以構造函數的初始化列表里不能初始化tail:
?
???template?<class?HT>
struct?cons<HT, null_type> {
...
????template<class?T1>
????cons(T1& t1,?const?null_type&,?const?null_type&,?const?null_type&,
????????const?null_type&,?const?null_type&,?const?null_type&,
????????const?null_type&,?const?null_type&,?const?null_type&)
????:?head?(t1) {} //?只初始化僅有的head
...
};
?
當參數被傳至最內層cons<>,一定是至少有尾部的九個null_type。這是因為如果你以N個模板參數來具現化tuple,則你初始化該tuple時最多只能提供N個參數,因為為N+i個參數準備的構造函數的第N+1至N+i個參數型別將推導為null_type(請回顧上面的各個構造函數,這是因為你沒有提供的模板參數都默認為null_type的緣故),而經過cons<>構造函數的重重“剝削”,直到最內層cons<>的構造函數被調用時,你給出的N個參數就只剩一個了(另外還有九個null_type)。所以這個偏特化版本的構造函數與上面的cons<>未特化版本中的并不相同。
這就是初始化的全過程。然而,事實上,在上例中,你不一定要將三個初始化參數全部給出,你可以給出0個1個或者2個。假設你這樣寫:
?
tuple<int,long,bool> MyTuple(10);
?
這將調用tuple的B構造函數,后者再將這唯一的參數后跟九個null_type傳給其基類—最外層的cons<>,這將使最外層的cons<>將其head初始化為10,然后—它將十個null_type傳給其tail的構造函數,而后者的head為long型數據成員,如果后者仍然使用上面給出的構造函數,則它會試圖用它接受的第一個參數null_type來初始化long?head成員,這將導致編譯錯誤,然而事實上這種初始化方式是語意上被允許的,對于這種特殊情況,cons<>提供了另一個構造函數:
?
???template?<class?T2,?class?T3,?class?T4,?class?T5,
??????????????class?T6,?class?T7,?class?T8,?class?T9,?class?T10>
?????cons(?const?null_type&?t1, //?當接受的第一個參數為null_type時
T2& t2, T3& t3, T4& t4, T5& t5,
????????????T6& t6, T7& t7, T8& t8, T9& t9, T10& t10 )
??????:?head?(),?tail?(t2, t3, t4, t5, t6, t7, t8, t9, t10, detail::cnull())
?????{}
?
如果提供的初始化參數“不夠”,十個參數將在cons<>的某一層(還不到最后一層)被“剝削”為全是null_type,這時將匹配cons<>的這個構造函數,它將head默認初始化(head(),而不是head(t1))。而cons<>的偏特化版本亦有類似的版本:
?
???cons(const?null_type&,
???????const?null_type&,?const?null_type&,?const?null_type&,
???????const?null_type&,?const?null_type&,?const?null_type&,
???????const?null_type&,?const?null_type&,?const?null_type&)
??: head () {}
?
這真是個隱晦繁復的過程,但愿你能理清頭緒。既然填充這幢基類“大廈”(cons<>)的材料(初始化tuple的參數)都能夠被安放到位。我們也得清楚如何再將它們取出來才是。這個“取”的過程又甚為精巧。
?
Tuple的取值過程
tuple允許你用這樣的方式取值:
?
someTuple.get<N>();??// get是模板函數
?
其中N必須得是編譯期可計算的常量。Boost庫的實現者不能實現這樣一個get版本——它允許你用一個變量指出想要獲取哪個元素:
?
someTuple.get(N);?????// N為變量-->錯誤
?
這個事實是有原因的,原因就在于get函數的返回值,你知道,用戶可以將不同形式的變量保存在tuple中,但是get函數是不能在運行期決定它的返回值的,返回值必須在編譯期就決議出來。然而用什么型別作為返回值呢?這取決于你想要保存的哪個對象。我們的例子:
?
tuple<int,long,bool> MyTuple;
?
中有三個變量。如果你寫MyTuple.get<0>()則該get的具現化版本的返回值將被推導為int。如果你寫MyTuple.get<1>()則這個get的具現化版本返回值將被推導為long。get的模板參數N就好象下標,不過卻是“型別數組”的下標。可見,get的返回值由其模板參數決定,而所有這些都在編譯期。這就是為什么你不能試圖用變量作“下標”來獲取tuple中的變量的原因。
顯然,我們很關心這個get模板函數是怎樣由它的模板參數(一個編譯期整型數)來推導出其返回值的。事實上,它通過一個traits來實現這點。下面是cons<>成員get函數的源代碼:
?
template?<int?N>
???typename?access_traits<???// access_traits<>上面已經講過
?????typename?element<N, cons<HT, TT> >::type //?element<>就是那個關鍵的traits
>::non_const_type //?注意這個復雜的返回類型
???get() {
?????return?boost::tuples::get<N>(*this);??//轉向全局的get<>函數
???}
?
所以我們下面跟蹤element<>的推導動作。請回顧我們的例子。假設我們現在寫:
?
MyTuple.get<2>();
?
這將導致tuple<int,long,bool>::get<2>()的返回值被推導為bool。下面就是如何推導的過程:
首先,最外層cons<>的HT=int,TT=cons<long,cons<bool,null_type> >;而調用的get正是最外層的。所以,上面的代碼中element<N,cons<HT,TT> >::type被推導為:
?
element<2,cons<int,cons<long,cons<bool,null_type> > > >::type
?
現在來看一看element<>的定義吧:
?
template<int?N,?class?T> //?這個int N會遞減,以呈現遞歸的形式
???struct?element
???{
???private:
?????typedef?typename?T::tail_type?Next;
???????????????????????//?在cons<>內部tail_type被typedef為TT,請回顧上面cons<>的代碼
???public:?????????????// cons<>內部有兩個關鍵的typedef:head_type、tail_type
?????typedef?typename?element<N-1,?Next>::type?type; //遞歸
???};
?
???template<class?T>
???struct?element<0,T>??//遞歸至N=0時,山窮水盡
???{
?????typedef?typename?T::head_type?type; //?山窮水盡時直接將head_type定義為type
???};
?
它看起來是如此的精巧簡練。其中的推導是這樣的:
?
element<>的內部有typedef?T::tail_type?Next;所以對于剛才我們推導出的:
?
element<2,cons<int,cons<long,cons<bool,null_type> > > >::type
?
其中的Next就是cons<int,cons<long,cons<bool,null_type> > >::tail_type,也就是:
?
cons<long,cons<bool,null_type> >
?
element中的type的typedef是這樣的:
?
typedef?typename?element<N-1,?Next>::type type;
?
對于本例,也就是:
?
typedef?typename?element<1, cons<long,cons<bool,null_type> > >::type?type;
?
同樣的方式,你可以推導出:
?
typename?element<1, cons<long,cons<bool,null_type> > >::type
?
其實就是:
?
typename?element<0,cons<bool,null_type> >::type
?
這下編譯器得采用element<>的偏特化版本了(因為第一個模板參數為0),根據偏特化版本的定義(其中對type的typedef為:typedef?typename?T::head_type?type;)你可以看出這實際就是:bool!
唔,經過重重剝削,element<>traits準確無誤的將第三個元素的型別萃取了出來!
再想一下,如果N為1,那么編譯器將這樣推導:
?
?typename?element<1, cons<int,cons<long,cons<bool,null_type> > > >::type
e???????typename?element<0, cons<long,cons<bool,null_type> > >::type
?
第二行編譯器會決定采用element<>的偏特化版本,從而這就是long!
這是個由typedef和整型模板參數的遞減所構筑的遞歸世界。編譯期的遞歸!(事實上,這種編譯期的編程被稱為metaprograming)現在你對這種遞歸方式應該有充分的自信。下面還有——真正取值的過程又是個遞歸調用的過程。類似的分析方法將再次采用。
請回顧上面給出的get<>的源代碼,其中只有一行——調用全局的get<>模板函數并將*this傳遞給它。所以重點是全局的get<>函數,它的源代碼是這樣的:
?
template<int?N,?class?HT,?class?TT>
???inline
typename?access_traits<??// access_traits<>的代碼請回顧上面
?????typename?element<N, cons<HT, TT> >::type
???>::non_const_type????????//?返回類型
???get(cons<HT, TT>& c) {???//?全局的get<>()函數
??????return?detail::get_class<N>::template?get<
?????????????????typename?access_traits<
?????????????????????typename?element<N, cons<HT, TT> >::type
?????????????????>::non_const_type
>(c);
}
?
你可以輕易看出玄機都在get_class<N>::template?get<>()上面。下面我將它的代碼挖給你看:
?
template<?int?N >??//這又是個用作遞歸之用的模板參數
???struct?get_class {
?????template<class?RET,?class?HT,?class?TT >
?????inline?static?RET get(cons<HT, TT>& t)
?????{
???????return?get_class<N-1>::template?get<RET>(t.tail);
?????}
???};
???template<>
???struct?get_class<0> {
?????template<class?RET,?class?HT,?class?TT>
?????inline?static?RET get(cons<HT, TT>& t)
?????{
???????return?t.head;
?????}
};
?
天哪,這真簡潔。因為遞歸能夠使程序變得簡潔。這里的遞歸仍然是通過遞減模板參數N實現,同時不斷將t.tail傳給get_class<N-1>::template get<RET>()直到N減為0,從而調用get_class<0>::get<RET>(),后者直接將t.head返回。就像這樣一種情境:(盒子表示cons<>,通常其中包括head元素和另一個盒子(cons<>)(除非是偏特化版本的cons<>))
有一排人,第一個人手里拿著一塊記數牌和一個盒子(記數牌上的數字表示模板參數N,盒子當然是cons<>數據容器)。現在,比如說,你告訴第一個人你像要那個盒子里的4號(第五個)元素(它深藏在第5重盒子里),他于是將記數牌上寫上4,然后再減去一,并將盒子打開一層,將里面的小盒子(t.tail,也是個cons<>容器,cons<>容器不正是一重套一重的嗎?)和記數牌一并傳給第二個人,第二個人將記數牌上的3減去一,然后再剝去一層盒子,將里面的盒子以及記數牌(現在是2了)傳給下一個人,下一個人做同樣的工作,直到第5個人(get_class<0>)發現記數牌上為0,那么他打開盒子,將里面的head元素傳給第四個,后者再傳給第三個...,一直傳至你手里。
并且,為了提高效率,get函數是inline的。
呼~是的,這真夠夸張,并且...不夠優雅!?是的,或許它的代碼非常丑陋,然而隱藏在它背后的思想確實無與倫比的優雅和精巧。更何況對于一個能夠應付千萬種情況,并具備高度復用性的類,這樣的實在可算是夠“優雅”的了。
另外boost還提供了一個length<>來獲得tuple的長度(即所含元素個數)
?
template<class?T>
???struct?length {
?????static const?int?value = 1 +?length<typename?T::tail_type>::value; //遞歸
};
???template<>
???struct?length<null_type> {
????static const?int?value = 0;
};
?
我想,有了上面的經驗,這種編譯期遞歸對于你應該了無秘密。我就不多說了。length<>位于namespace tuples里面。
?
最后一點細節
??為了方便用戶,boost庫還提供了make_tuple和tie函數,前者很簡單:產生一個臨時的tuple,你可以這樣使用它:
?
???tuple<int,long,bool> MyTuple=make_tuple(10,10,true);
?
??而tie則意為將參數綁在個tuple里面,不同的是因為是綁,所以它返回的tuple保存引用,像這樣使用它:
?
???int ival=10;??long lval=10; bool bval=true;
???tuple<int&,long&,bool&> MyTuple=tie(ival,lval,bval);
???... //?這里,你修改MyTuple里的數據會直接影響到ival,lval,bval;
?
??你還可以用一行代碼來更改三個變量的值,像這樣:
?
???tie(ival,lval,bval)=make_tuple(9,9,false); //?同時更改了三個變量值
?????????????????????????????????????????????//?現在ival,lval,bval分別為9,9,false。
?
??你還可以忽略make_tuple()返回的部分值,像這樣:
???tie(ival,tuples::ignore,bval)=make_tuple(9,9,false);
//?只有ival,bval被更改,lval維持原值
??????// tuples::ignore是個預定義的對象,它有一個模板化的operator =函數,
//?從而可以接受向它賦的任何值。
?
本文沒有涉及的
本文沒有涉及tuple對IO的支持——實際上它幾乎只是對tuple中的每一個元素進行輸出。
?
本文沒有涉及tuple的拷貝構造函數,cons<>的拷貝構造函數,以及cons<>的const成員函數——事實上,在了解了以上那些秘密后,這就微不足道了。
?
本文沒有涉及tuple提供的比較函數——事實上那比較簡單,它只是轉而比較各個元素。
?
目錄(展開《boost源碼剖析》系列文章)
總結
以上是生活随笔為你收集整理的boost源码剖析之:Tuple Types(rev#2)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: boost源码剖析之:boost::mu
- 下一篇: boost源码剖析之:泛型函数指针类bo