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