超越boost: 1)tuple

写这个系列包括下列原因:

1)boost很多东西很有用,真的。

2)boost很多东西用起来却很痛苦。不出问题则以,一出问题,编译时错误信息1M算少的了;调试进入boost代码,你就等着两眼摸黑了吧。

3)boost很多东西的源码值得学习,如果你能完整的完成一个库,几乎可以吃透template绝大部分知识了。笔者完完整整实现完了的东西也就typeof,tuple还有一些比较简单的东西。

4)boost很多东西很长时间不更新了,发稿过去也爱理不理的。以这个tuple为例,别人回信说:“...虽然你的实现后端更高效,不过tuple已经列入tr1,在很多开发平台stl中实现了,没有必要更新了。我们现有的实现在经过高效编译器的优化后和你的实现效率相差不大,贸然替换我们还有海量的兼容性测试要做...”不提了。

5)boost很多东西代码风格很混乱。有一些代码过度的换行(其实就是比较垃圾的反复重载,特化代码,T1,T2,T3,T4,T5,T6,T7,T8,T9,T10....,一个参数要占一行),使得阅读一点代码不得不反复的翻屏,极难以阅读。

 

之所以要拿tuple开头,是因为它很有趣,很有用,对其他boost库的依赖性也相对比较小。而且是唯一一个笔者敢自称做的比boost好的一个库!

 

首先介绍一些背景,最好你还能看看这些

1)tuple是一种多元数据容器,是std::pair的扩展,boost::tuple支持最多10元的容器,语义和接口几乎和python等语言的tuple等价。

2)tuple的实现类似于typelist,然而它是数据容器,所以还需要提供数据访问和本身构造的接口。

3)boost::tuple的类型系统和loki::typelist类似,内部数据结构则使用了“组合”(Composition)构建这个“虚”list(由于是组合而不是聚合,从实现上看,空间上实际是连续的),访问特定元素却需要遍历这个链(这个空间上的连续关系编译器基本上都是明白的,但c++语言不对程序员开放这个接口,所以只能寄希望于编译器优化掉这个遍历行为)。

这里的3)是什么意思呢:见下例

 

struct A0 {};

struct A1 {  A0 last_data; std::string data; const std::string &get_data1() const;};

struct A2 {  A1 last_data; int data;             const std::string &get_data1() const;};

struct A3 {  A2 last_data; double data;      const std::string &get_data1() const;};

 

这里A0可以看作是一个数据链表中的空节点,Ai+1和Ai是组合的关系,空间上是连续的。这基本就是boost::tuple的实现的数据模型。我们接下来就可以把A3看作是一个3元的tuple,有3个data的元素,也就是boost::tuple<std::string, int, double>.

 

对于一个A3类型的对象a3而言,他要访问第“1”个元素该怎么访问呢?

首先要说明的一点是, 实际的实现中A1, A2, A3的data类型都是范性的,所以不可以假设A1, A2, A3符合POD语义(如A1,data是非POD).既然不能轻易使用OFFSET之类的方法访问,那自然只有唯一一种方法了:

a3.last_data.last_data.data

这对于编译器来说,3步路实际上是一步路:它很清楚我们需要访问a3首地址那块sizeof(string)的数据。

 

然而回到传统的链表:学过链表的人都知道,我们的list是不定长的,当传入一个非负整数参数N,访问链表的第N个元素时,我们显然不能把访问函数std::list::at(int)实现成

p->next->next->next...->data           //N个-〉,其中p是链表头指针,next是下一个节点的指针,然而代码是固定的呀,所以不能这样写代码。

 

tuple的长度虽然是编译时确定,但是对于范型代码本身而言,我们的tuple也将是不定长的,如果为N =1 , N =2 ,N =3, N= 4..........N=10各自特化访问,不仅代码冗长,而且还要对每个类加上额外的代码控制特化代码的产生(例如tuple 长度为5时,就不能产生N= 6--10的特化代码,因为是全特化,类一实例化函数会立即实例化。加一个控制参数是一种方法,当然开销也来了),那么产生的特化访问代码将多达10*(10-1)/2= 45种,够骇人听闻吧。

 

boost::tuple怂了,使用了连续的函数调用(当然实际的实现是用模板的范型,这里简单化问题,只需要访问第一个。这里列出所有的实例化结果) 

inline const std::string &A3::get_data1() const {return last_data.get_data1();}

inline const std::string &A2::get_data1() const {return last_data.get_data1();}

inline const std::string &A1::get_data1() const {return data;}                  //实际代码当然使用了特化来控制迭代的结束。

 

a3.get_data1()代码的语义和a3.last_data.last_data.data没有区别,问题是编译器能够这么聪明,理解咱们的意思,把3个函数甚至更多个函数不仅要inline, 而且还要合并他们, 实现a3.get_data1()转为a3.last_data.last_data.data吗?大家现在还敢保证A3类型的对象a3能一步就访问到第一个元素吗?

 

笔者试了一下gcc4以下不能,vc2008在步数多于5时不能。毕竟这已经属于很深层次的语义分析了,和一般的尾递归还有所不同,如果选择的数据类型更凌乱,更复杂一些,还有union,访问中间的元素,只怕更难。咱们也不要太过于苛求编译器,否则编译时间会让你郁闷致死....而且这里还产生了额外的多线程竞争访问的危险.

 

 

笔者在此给出了自己的优化和理解:使用继承取代组合。

 

笔者当时因为需要写一个数学运算函数式而不得不实现一个类似的东西来封装传入的多元函数变量的值,后来一看boost::tuple:不就是那么一回事嘛,效率还不如我的家伙。然后把接口名字什么的大致改的和tuple差不多,就直接投稿了。然后当然如上文所说,被别人很客气的鄙视了.

 

为什么继承好:第一,这是内部细节,加强耦合完全不是什么问题;第二,当然就是可以"一步"访问,可以理解为随机访问,random_access!

 

看下新的实现:

struct A0 {};

struct A1 : public A0  {  std::string data; const std::string &get_data1() const;};

struct A2 : public A1  {  int data;             const int  &get_data1() const;};

struct A3 : public A2  {  double data;      const double &get_data1() const;};

 

inline const std::string &A3::get_data1() const {return ((const A1*)this)->data;} //1步!

inline const std::string &A2::get_data1() const {return ((const A1*)this)->data;} //1步!

inline const std::string &A1::get_data1() const {return ((const A1*)this)->data;} //1步!

 

不仅时间效率提升了,而且代码也清爽了,用特化来终止迭代也不用了,空间效率只可能比原来高或者相等(对于正常的编译器,组合和继承产生的对象大小应该相等的) 。问题就只剩一个: 把输入的N通过模板的种种" 幻术" 映射为类型AN, 小case! 略过不提.

 

贴上我的代码,这里提供的是不依赖任何外部头文件的实现. 所以没有提供对于std::pair的赋值和tie, 对于iostream的流操作符重载.以及没有什么大用的ignore对象, 对引用类型的存储(tuple的定位就应该是一个数据容器,别太花哨).其他接口和boost::tuple一致.有些比较必须的垃圾重载代码直接找boost拷贝了过来.

 

直接编译就可以运行,大家可以随便修改main函数,第一次用tuple的可以享受一下.已经在vc6,2005,icc10,g++等通过测试.

 

 

控制宏: 

ENABLE_TIE,如果你不需要tie操作,  注释掉它. tie会给整体的代码带来一些小开销.笔者也觉得这个东西比较花腔,没有多大价值.

ENABLE_PARTIAL_SPECIALIZATION 如果你的编译器(如vc6)不支持偏特化, 注释掉它.笔者另外利用内部类来帮你模拟.

 

注意请不要在vc6里面编译调试版,好像是调试导出的symbol过长产生了溢出或者截断,然后会产生莫名其妙的编译错误.release版编译没有问题,为了提高编译速度,请关闭所有警告.

 

题外话: 笔者在这里的typelist实现中玩弄了一个类似c++ 0x的右值引用"类型折叠"的技巧(确实是受了右值引用这个设计思想的影响), 自动清除和合并typelist中的多余的空白节点null_type,一时颇为自傲,瞧不起boost::tuple. 后来某次再仔细瞅了瞅boost::tuple的实现, 原来别人也有独立实现的嘛(确实boost源码可读性不高).一种英雄惜英雄, 码农惜码农的感觉不由得油然而生....

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值