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

时间:2021-07-12 05:59:56
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数据成员就是需要分配内存的主体。如果将这种布局扩展,大概就像这样:

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

这种布局正像一种玩具----开始是一个盒子,揭开盒子其内部又是个更小的盒子,再揭,还是盒子...

现在,基类的内存布局已经展现在你面前。这一切其实就是由那个魔棒般的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 本文没有涉及的

 

  1. 本文没有涉及tuple对IO的支持----实际上它几乎只是对tuple中的每一个元素进行输出。
  2. 本文没有涉及tuple的拷贝构造函数,cons<>的拷贝构造函数,以及cons<>的const成员函数----事实上,在了解了以上那些秘密后,这就微不足道了。
  3. 本文没有涉及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++设计新思维》)对其有详细的介绍,并对贯穿本文的“编译期递归”思想有很好的阐述。