boost 源码剖析之: Tuple Types(rev#2)
刘未鹏 (pongba)
C++ 的罗浮宫 (http://blog.csdn.net/pongba)
Note: 并非新作, 04 年曾放在 blog 上,后来删掉了,不过网上到处有转载。这是修改之后的版本。
动机 [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 的限制,于是你可以写:
//tuple<> 目前能够支持多达 10 个模板参数
boost::tuple < int , int , int > someFunc();
事实上 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 的设计细节和最本质的东西 —— 源代码。
设计目标
首先,了解 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++ 泛型几乎完全是编译期的。
其实,虽然上面我只为你描述了两个设计目标,但是实作时仍会有各种小问题出现。下面的源码剖析中我会一一为你解惑。
好吧,在你发出抱怨声之前,我还是快点转入我们的主题:
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 只是将参数转交给基类处理。下面我为你剖析它的基类:
基类大厦的构建
构建大厦的脚手架 ——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
{
// cons <> 是数据的容器,也是所有奥秘所在
1 typedef cons <
T0 , // 第一个参数 T0 被孤立出来
typename map_tuple_to_cons< // 剩下的模板参数后跟一个 null_type 进入下一轮
T1, T2, T3, T4, T5,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 >::type
>
>
>
好了,该是结束这场游戏的时候了,你应该看出来了,
map_tuple_to_cons<>
准备了一个特化版本来作为这场类似绕口令的递归式包含的休止符。所以,以上的定义再作最后一重推导,使用
2
处的
typedef
,将
typename
map_tuple_to_cons<
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 的定义中充当了十分重要的角色,如果没有它的介入,难道还有更简洁美妙的方式来达到这个目的吗?
构建大厦的砖石 ——cons<>
现在,你一定非常想看一看 cons <> 的定义,下面就是:
template < class HT, class TT>
struct cons {
typedef HT head_type; // 这是个用户提供的型别
typedef TT tail_type; // 这通常是个 cons<> 的具现体
// 以上两个 typedef 很重要,并非可有可无
typedef
typename 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 ;
// 注意,不像上面的主模板,这里没有 tail 成员
... // 成员函数将在后面解释
};
根据 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< void > 这样的具现化肯定会惹恼编译器 ( 因为它将会发觉 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 // 如果不是函数类型则 type 就是 T
>::RET type;
};
以及其特化版本:
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 < long , cons < 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 数据成员就是需要分配内存的主体。如果将这种布局扩展,大概就像这样:
<shapetype id="_x0000_t75" stroked="f" filled="f" path="m@4@5l@4@11@9@11@9@5xe" o:preferrelative="t" o:spt="75" coordsize="21600,21600"><stroke joinstyle="miter"></stroke><formulas><f eqn="if lineDrawn pixelLineWidth 0"></f><f eqn="sum @0 1 0"></f><f eqn="sum 0 0 @1"></f><f eqn="prod @2 1 2"></f><f eqn="prod @3 21600 pixelWidth"></f><f eqn="prod @3 21600 pixelHeight"></f><f eqn="sum @0 0 1"></f><f eqn="prod @6 1 2"></f><f eqn="prod @7 21600 pixelWidth"></f><f eqn="sum @8 21600 0"></f><f eqn="prod @7 21600 pixelHeight"></f><f eqn="sum @10 21600 0"></f></formulas><path o:connecttype="rect" gradientshapeok="t" o:extrusionok="f"></path><lock aspectratio="t" v:ext="edit"></lock></shapetype><shape id="_x0000_i1025" style="WIDTH: 5in; HEIGHT: 168.75pt" type="#_x0000_t75"><imagedata o:title="tuple" src="file:///C:/DOCUME~1/cface/LOCALS~1/Temp/msohtml1/01/clip_image001.jpg"></imagedata></shape>
这种布局正像一种玩具 —— 开始是一个盒子,揭开盒子其内部又是个更小的盒子,再揭,还是盒子 ...
现在,基类的内存布局已经展现在你面前。这一切其实就是由那个魔棒般的 map_tuple_to_cons<> 所造就的,它建造了这种嵌套式的结构。这样构建的好处就是嵌套的重数可以由用户给出的模板参数个数来控制。前者体现了底层内存的占用量 ( 如果重数为 N 重,则只有 N 个 head 占用内存 ) ,后者体现用户的需求量。这正是一种 “ 按需分配 ” 。
在基类的大厦构架完毕后,问题自然是,如何将材料填入这幢蜂窝般的大厦。这得从 tuple 的构造函数入手,下面我就带你作一次跟踪。
初始化的全过程
然而在跟踪之前我们须了解 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() {} // 这里也调用 基类 的默认构造函数
// access_traits<> 的定义后面解释
tuple( typename access_traits < T0 >::parameter_type t0)
: inherited(t0, detail::cnull(), // cnull 函数返回 null_type() 对象
detail::cnull(), detail::cnull(), // 可将 detail::cnull() 看作 null_type()
detail::cnull(), detail::cnull(),
detail::cnull(), 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, // 当接受的第一个参数为 null_type 时
T2& t2, T3& t3, T4& t4, T5& t5,
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 的参数 ) 都能够被安放到位。我们也得清楚如何再将它们取出来才是。这个 “ 取 ” 的过程又甚为精巧。
Tuple 的取值过程
tuple 允许你用这样的方式取值:
someTuple. get <N>(); // get 是模板函数
其中 N 必须得是编译期可计算的常量。 Boost 库的实现者不能实现这样一个 get 版本 —— 它允许你用一个变量指出想要获取哪个元素:
someTuple.get(N); // N 为变量 --> 错误
这个事实是有原因的,原因就在于 get 函数的返回值,你知道,用户可以将不同形式的变量保存在 tuple 中,但是 get 函数是不能在运行期决定它的返回值的,返回值必须在编译期就决议出来。然而用什么型别作为返回值呢?这取决于你想要保存的哪个对象。我们的例子:
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 get<
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 的。
呼~是的,这真够夸张,并且 ... 不够优雅!?是的,或许它的代码非常丑陋,然而隐藏在它背后的思想确实无与伦比的优雅和精巧。更何况对于一个能够应付千万种情况,并具备高度复用性的类,这样的实在可算是够 “ 优雅 ” 的了。
另外 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 里面。
最后一点细节
为了方便用户, 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 = 函数,
// 从而可以接受向它赋的任何值。
本文没有涉及的
本文没有涉及 tuple 对 IO 的支持 —— 实际上它几乎只是对 tuple 中的每一个元素进行输出。
本文没有涉及 tuple 的拷贝构造函数, cons<> 的拷贝构造函数,以及 cons<> 的 const 成员函数 —— 事实上,在了解了以上那些秘密后,这就微不足道了。
本文没有涉及 tuple 提供的比较函数 —— 事实上那比较简单,它只是转而比较各个元素。
目录(展开 《 boost 源码剖析》系列 文章)