Boost源码剖析之:增强的std::pair--Tuple Types (一)

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库的设计无疑是最精致和高效的),容我向你阐述它们:
  1. 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设施的手法来定义它的底层容器,这种精致的手法利用了某种递归的概念,极大的减少了代码量。后面我会为你介绍它。
  2. 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的构造函数入手,下面我就带你作一次跟踪。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值