boost源码剖析之:Tuple Types(rev#2)

本文深入探讨Boost库中的TupleTypes实现细节,通过源码解析,揭示其设计目标及其实现技巧,如利用递归概念减少代码量,以及通过编译期递归获取内部数据等。

boost源码剖析之:Tuple Types(rev#2)

刘未鹏(pongba)

C++的罗浮宫(http://blog.youkuaiyun.com/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能够提供的不止这个,tupleIO流的支持能够允许你写这样的代码:

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,

### Boost.PFR 中 `tuple_size` 的定义与用法 #### 定义 Boost.PFR (Portable Format Representation) 提供了一种机制来操作结构体中的字段,类似于标准库中对 `std::tuple` 的支持。其中,`boost::pfr::tuple_size` 是一个模板类,用于计算给定类型的成员数量。它的主要功能是对任意具有公共数据成员的结构体或联合体进行反射式的大小检测。 通过 `tuple_size<T>::value` 或其别名模板 `tuple_size_v<T>`,可以静态地获取某个类型所表示的数据成员总数[^1]。 #### 使用方法 以下是两个具体的例子展示如何使用 `boost::pfr::tuple_size`: ##### 示例 1:基本用法 以下代码展示了如何利用 `boost::pfr::tuple_size_v` 来获取自定义结构体的成员数。 ```cpp #include <iostream> #include <string> #include <boost/pfr.hpp> struct person { int age; std::string name; }; int main() { using boost::pfr::tuple_size_v; person p = {32, "Alice"}; constexpr std::size_t size = tuple_size_v<person>; std::cout << "Number of members in person structure: " << size << '\n'; return 0; } ``` 上述代码会输出: ``` Number of members in person structure: 2 ``` 这里的关键在于 `tuple_size_v<person>` 返回的是 `person` 结构体中公开可访问的数据成员数目,即 `age` 和 `name`。 ##### 示例 2:位域的支持 如果目标类型是一个带有位域的结构体,则可以通过类似的手段验证其成员数量。例如: ```cpp #include <boost/pfr/tuple_size.hpp> struct bf { unsigned int i1 : 1; unsigned int i2 : 1; unsigned int i3 : 1; unsigned int i4 : 1; unsigned int i5 : 1; unsigned int i6 : 1; }; static_assert(boost::pfr::tuple_size<bf>::value == 6, ""); ``` 此代码片段确认了 `bf` 类型有六个独立的位域成员,并且这些都可以被 `boost::pfr::tuple_size` 正确识别出来[^2]。 #### 技术细节 - **编译期求值**:由于 `tuple_size` 基于模板元编程技术实现,因此它可以完全在编译期间完成评估工作。 - **适用范围**:除了普通的 POD(Plain Old Data)类型外,还能够很好地兼容含有位域或其他复杂布局的结构体。 - **错误处理**:当尝试查询不合法或者无法解析的对象时,通常会导致编译失败并提示相应的诊断信息。 ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值