荔园在线

荔园之美,在春之萌芽,在夏之绽放,在秋之收获,在冬之沉淀

[回到开始] [上一篇][下一篇]


发信人: bso (BSO's So Open), 信区: Program
标  题: Boost源码剖析之:增强的std::pair--Tuple Types
发信站: 荔园晨风BBS站 (Wed Aug 16 10:57:28 2006), 站内

作者:ppLiu(刘未鹏)
来源:http://blog.csdn.net/pongba/archive/2004/08/24/83813.aspx
Boost源码剖析之:增强的std::pair--Tuple Types
0 动机[1]
1 设计目标
2 boost::tuple源码剖析
3 基类大厦的构建
3.1 构建大厦的脚手架----map_tuple_to_cons<>
3.2 构建大厦的砖石----cons<>
4 初始化的全过程
5 Tuple的取值过程
6 最后一点细节
7 本文没有涉及的
0 动机[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的限制,于是你可以写:

boost::tuple<int,int,int> someFunc(); //tuple<>目前能够支持多达10个参数事实上
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的设计细节和最本质的东西----源代码。
1 设计目标
首先,了解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++泛型几乎完全是编译期的。
其实,虽然上面我只为你描述了两个设计目标,但是实作时仍会有各种小问题出现。下面的
源码剖析中我会一一为你解惑。

好吧,在你发出抱怨声之前,我还是快点转入我们的主题:

2 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只是将参数转交给基类处理。下面我为你剖析它的
基类:
3 基类大厦的构建
3.1 构建大厦的脚手架----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
{
        1  typedef cons<T0,   //cons<>是数据的容器,也是所有奥秘所在,第一个参数
T0被孤立出来
        typename map_tuple_to_cons<T1, T2, T3, T4, T5, //剩下的模板参数后跟一个
null_type
        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,...,
null_type>::type> > >好了,该是结束这场游戏的时候了,你应该看出来了,
map_tuple_to_cons<>准备了一个特化版本来作为这场类似绕口令的递归式包含的休止符。
所以,以上的定义再作最后一重推导,使用2处的typedef,将typename
map_tuple_to_cons< null_type,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的定义中充当了十分重要的角色,如果没有它的介入,难
道还有更简洁美妙的方式来达到这个目的吗?

3.2 构建大厦的砖石----cons<>
现在,你一定非常想看一看cons<>的定义,下面就是:
template <class HT, class TT>
struct cons {
        typedef HT head_type; //这是个用户提供的型别
        typedef TT tail_type; // 这通常是个cons<>的具现体
        typedef typename  //以上两个typedef很重要,并非可有可无
        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;
        ... //成员函数将在后面解释
};根据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这样的具现化肯定会惹
恼编译器(因为它将会发觉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 //如果为函数类
型则特殊处理
        >::RET type;                                    //如果不是函数类型则
type就是T
};以及其特化版本:
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< <nop> long,cons< <nop> 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
的构造函数入手,下面我就带你作一次跟踪。


4 初始化的全过程
然而在跟踪之前我们须了解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() {} //这里也调用基类的默认构造函数
        tuple(typename access_traits<T0>::parameter_type t0) //access_traits<>的
定义后面解释
        : inherited(t0, detail::cnull(), detail::cnull(), detail::cnull(),
//cnull函数返回null_type()对象
        detail::cnull(), detail::cnull(), detail::cnull(), //可以将
detail::cnull()看作null_type()
        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,则typename
access_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, T2& t2, T3& t3, T4& t4, T5& t5,  //当接受的第一个参数
为null_type时
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的参数)都能够被安放到位。我们也得清楚如何再将它们取出
来才是。这个“取”的过程又甚为精巧。

5 Tuple的取值过程
tuple允许你用这样的方式取值:
someTuple.get<N>();  //get是模板函数其中N必须得是编译期可计算的常量。Boost库的实
现者不能实现这样一个get版本----它允许你用一个变量指出想要获取哪个元素:
someTuple.get(N); //N为变量-->错误

这个事实是有原因的,原因就在于get函数的返回值,你知道,用户可以将不同形式的变量
保存在tuple中,但是get函数是不能在运行期决定它的返回值的,返回值必须在编译期就决
议出来。然而用什么型别作为返回值呢?这取决于你想要保存的哪个对象。我们的例子:
%CODE{"cpp"}%
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
&#240;      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  //这个template关键字指出
getclass<N>::get为内嵌模板
        get<     //这个get<>()函数是get_class<>的静态成员模板函数
        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的。

呼~~~是的,这真够夸张,并且...不够优雅!?是的,或许它的代码非常丑陋,然而隐藏在
它背后的思想确实无与伦比的优雅和精巧。更何况对于一个能够应付千万种情况,并具备高
度复用性的类,这样的实在可算是够“优雅”的了。正如候捷先生在《深入浅出MFC》里所
说的,就像制作蜜饯,蜜饯好吃,但是其制作过程呢?唔...我不知道。^_^。

另外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;里面。
好了,所有秘密都展示在你面前了。正如候捷先生在《深入浅出MFC》里所说的:


山高月小 水落石出

6 最后一点细节
为了方便用户,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 =函数,从而可以接受向
它赋的任何值。7 本文没有涉及的

本文没有涉及tuple对IO的支持----实际上它几乎只是对tuple中的每一个元素进行输出。
本文没有涉及tuple的拷贝构造函数,cons<>的拷贝构造函数,以及cons<>的const成员函数
----事实上,在了解了以上那些秘密后,这就微不足道了。
本文没有涉及tuple提供的比较函数----事实上那比较简单,它只是转而比较各个元素。
注释

[1] C++ Users Journal 上面有一篇关于Tuple的报告文章,出自Herb Sutter(《
Exceptional C++》和《More Exceptional C++》的作者)之手。
http://www.cuj.com/documents/s=8250/cujcexp2106sutter/

[2] Loki库出自Andrew Alexandrescu之手,由他执笔并由候捷,於春景翻译的《Modern
Design C++》(中文名《C++设计新思维》)对其有详细的介绍,并对贯穿本文的“编译期递
归”思想有很好的阐述。


--
  →  小白脸公司正式成立.....en.....来吧  ←
    →玩游戏?炒股?灌水?---->>>>>梦想农庄欢迎各位的光临!!!!!!←
↘                                          ↙
    →http://192.168.116.111/LTM/
 【 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓  】


※ 来源:·荔园晨风BBS站 bbs.szu.edu.cn·[FROM: 192.168.116.111]


[回到开始] [上一篇][下一篇]

荔园在线首页 友情链接:深圳大学 深大招生 荔园晨风BBS S-Term软件 网络书店