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

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++设计新思维》)对其有详细的介绍,并对贯穿本文的“编译期递归”思想有很好的阐

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值