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<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<int,int,int> t(8,9,10); std::cout<<t; //输出(8 9 10)
std::cout<<tuples::set_open(‘[‘)<<tuples::set_close(‘]’)<<tuples::set_delimiter(‘,’)<<t;
//输出[8,9,10]
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); //请记住它,后面我们将一直围绕这个例子
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; //注意这个声明的所有模板参数都有缺省值
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的基类
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);
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; };
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
cons<int,typename map_tuple_to_cons<long,bool,null_type,...,null_type>::type>
cons<int,cons<long,typename map_tuple_to_cons<bool,null_type,...,null_type>::type> >
cons<int,cons<long,cons<bool,typename map_tuple_to_cons<null_type,null_type,...,null_type>::type> > >
cons<int,cons<long,cons<bool,null_type> > > //这实际上只为int,long,bool各分配一份空间
map_tuple_to_cons<>以一种递归的方式不断将它的第一个模板参数割裂出来,并使tuple的基类呈现像这样的形式:
cons<T0,cons<T1,cons<T2,cons<T3,... ... > > > >
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; //第二个数据成员 ... //其成员函数将在后面解释,此处先略去 };
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; ... //成员函数将在后面解释 };
detail::wrap_non_storeable_type<head_type>::type //head_type又被typedef为HT
HT head; //如果HT为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; };
template <class T> class non_storeable_type { non_storeable_type(); //仅有私有的构造函数,意味着不能拥有该类的对象实体 };
所有这些正符合void及函数型别的特性----能够被typedef,却不能拥有数据对象实体。(boost的实现者可真够细心的)
好了,从细节中回过神来。我们通常显然不会用void和函数型别来具现化tuple。所以,通常,cons<>内部的两个数据成员的型别通常其实就是:
HT head; TT tail;
cons<int,cons<long,cons<bool,null_type> > >
typename HT=int,typename TT= cons< <nop> long,cons< <nop> bool,null_type> >
int head; cons<long,cons<bool,null_type> > tail; //注意这又是一个cons<>对象
long head; cons<bool,null_type> tail;
bool head;
这种布局正像一种玩具----开始是一个盒子,揭开盒子其内部又是个更小的盒子,再揭,还是盒子...
现在,基类的内存布局已经展现在你面前。这一切其实就是由那个魔棒般的map_tuple_to_cons<>所造就的,它建造了这种嵌套式的结构。这样构建的好处就是嵌套的重数可以由用户给出的模板参数个数来控制。前者体现了底层内存的占用量(如果重数为N重,则只有N个head占用内存),后者体现用户的需求量。这正是一种“按需分配”。
在基类的大厦构架完毕后,问题自然是,如何将材料填入这幢蜂窝般的大厦。这得从tuple的构造函数入手,下面我就带你作一次跟踪。