被误解的C++——优化variant实现

longshanks 2007-06-28 03:37:47
优化variant实现
上一次,我大概制作了一个variant类型,并设法赋予这个类型同C++内置类型几乎一样的行为。但是,具体实现起来,倒是有点望而生畏。想想看,如果我的variant需要包容5种类型,那么单单一个操作符,就需要5×5+1=26个操作符重载(那单独一个是variant类型操作数的重载)。所有二元操作符都是如此。
通过蛮力来实现variant,尽管可能,但着实愚蠢。我们必须寻找更简单有效的实现途径,避免为了一个“屁眼大的”variant(请原谅我说粗话)写上几万行代码,而且这些代码就像一窝小猪仔那样相像。好在C++为我们提供了充足的现代武器,使我们拥有足够的火力摆平这些问题。
让我们先从操作数都是variant的二元操作符入手:
variant operator+( const variant& v1, const variant& v2) {…}

简单起见,先考察operator+的实现,然后扩展到其他操作符。
由于操作数是variant类型,那么它们可能代表不同的类型。我们必须知道操作数的实际类型,才能对其实施相应的+操作。最传统的办法就是使用switch:
variant operator+(const variant& v1, const variant& v2) {
switch(v1.get_type_code())
{
case vt_double:
switch(v2.get_type_code())
{
case vt_double:
…;
break;

}
case vt_int:
switch(v2.get_type_code())
{
case vt_double:
…;
break;

}

}
}
好家伙,又是一个组合爆炸。一步步来,我们先来处理这堆讨人嫌的switch…case…。一般而言,对于一个函数(操作符)内的的大量分派操作,可以使用包含函数指针的数组或者容器替代。如果标记值(这里的vt_...)是连续的,可以直接使用数组;如果标记值不连续,可以使用关联容器。这里vt_...是连续的,所以用数组比较方便:
typedef variant (*add_op_t)(const variant& v1, const variant& v2);
add_op_t tbl_type_ops[3][3]; //函数指针表,假设variant对应三种类型
variant add_op_double_double(const variant& v1, const variant& v2){…}
variant add_op_double_int(const variant& v1, const variant& v2){…}

variant add_op_int_double(const variant& v1, const variant& v2){…}

tbl_type_ops [vt_double][vt_double]=add_op_double_double;
tbl_type_ops [vt_double][vt_int]=add_op_double_int;

variant operator+(const variant& v1, const variant& v2) {
return tbl_type_ops [v1.get_type_code()][v2.get_type_code](v1, v2);
}
operator+的代码是简单了,但是它的代码实际上转嫁到每个专用操作函数add_op_...上去了。并没有简化多少。下一步,我们来处理这些add_op_...:
template<typename VT1, typename VT2>
variant add_op(const variant& v1, const variant&v2) {
throw exception(string(“cannot add type ”)+typeid(VT1).typename()
+”to”+typeid(VT2).typename());
} //主函数模板,对应不兼容类型的操作。抛出异常。
template<>
variant<double, double> add_op(const variant& v1, const variant&v2) {
return variant(v1.dbval+v2.dbval);
} //针对double+double的操作

tbl_type_ops [vt_double][vt_double]=add_op<double, double>;
tbl_type_ops [vt_double][vt_int]=add_op<double,int>;

利用函数模板,及其特化,消化掉一部分的冗余代码。利用主函数模板实现所有不能互操作的类型操作,而可操作的类型则使用特化的模板实现。当然,冗余代码还是存在,这部分我们一会儿再处理。先来看看tbl_type_ops的填充。这部分代码也存在组合爆炸。为消除这个问题,我请出了模板元编程(TMP)。当然,我没有那么好的本事去直接倒腾TMP,我“借用”了boost::mpl::vector来实现这步优化:
//使用mpl::vector存放variant包容的类型
typedef boost::mpl::vector<double, int, string> op_types;
const int n_types=boost::mpl::size<op_types>::value;
//操作函数指针表
typedef variant (*add_op_t)(const variant& v1, const variant& v2);
add_op_t tbl_type_ops[n_types][n_types];
//填充函数指针表单个元素
template<int m, int n>
inline void set_tbl_type() {
typedef mpl::deref<mpl::advance<mpl::begin<op_types>::type,
mpl::int_<m> >::type>::type type_1;
typedef mpl::deref<mpl::advance<mpl::begin<op_types>::type,
mpl::int_<n> >::type>::type type_2;

tbl_type_ops [m][n]=add_op<type_1, type_2>;
}
//填充函数指针表单元的函数对象类
template<int m, int n>
struct fill_tbl_types_n
{
void operator()() {
set_tbl_type<m-1, n-1>(); //填充函数指针单元
fill_tbl_types_n<m, n-1>()(); //递归
}
};
template<int m>
struct fill_tbl_types_n<m, 0> //特化,递归结束
{
void operator()() {}
};
//填充函数指针表行的函数对象类
template<int m, int n>
struct fill_tbl_types_m
{
void operator()() {
fill_tbl_types_n<m, n>()(); //创建并调用fill_tbl_types_n函数对象
fill_tbl_types_m<m-1, n>()(); //递归
}
};
template<int n>
struct fill_tbl_types_m<0, n> //特化,递归结束
{
void operator()() {}
};
void fill_tbl_op() {
fill_tbl_types_m<n_types, n_types>()();
}
这里运用函数对象类模板的特化,构造了函数指针表的填充自动函数。在需要时,只需调用fill_tbl_op()函数即可。该函数中创建fill_tbl_types_m<n_types, n_types>函数对象,然后调用。这个函数对象的operator()首先创建并调用fill_tbl_types_n<m, n>函数对象。后者先调用set_tbl_type<m-1, n-1>模板函数,执行填充tbl_type_op数组的[m-1, n-1]单元格。然后递归调用fill_tbl_types_n<m, n-1>函数对象。直到n-1==0,编译器便会选择特化版本的fill_tbl_types_n<m, 0>函数对象。该特化的operator()操作符重载是空的,因此递归结束。这样完成一行的填充。然后,fill_tbl_types_m<m, n>则递归调用fill_tbl_types_m<m-1, n>函数对象,填充下一行。直到调用fill_tbl_types_m<0, n>特化版本,结束递归。
现在需要仔细看一下set_tbl_type<>函数模板。该模板上来就是两个typedef。这两个typedef创建了两个类型别名,分别用m和n做索引,从boost::mpl::vector<double, int, string>中取出相应的类型:
typedef mpl::deref<mpl::advance<mpl::begin<op_types>::type,
mpl::int_<m> >::type>::type type_1;

头晕是吧。我的头还有点晕呢。这就是模板元编程,不停地鼓捣类型。具体的操作可以参考boost文档或《The Template Meta-programming》一书,我这里就不多说了,反正就是从一个存放类型的vector中取出所需的类型。
这样获得的两个类型用来实例化add_op<>()模板函数,并且填充到tbl_type_ops[m][n]元素中。
这样,利用TMP和GP两种强大的机制,消除了tbl_type_ops填充的组合爆炸问题。如果我们需要向variant中加入新的类型,那么只需在mpl::vector<double, int, string>中直接加入类型即可:
typedef mpl::vector<double, int, string, bool, datetime> op_types;
OK,下面回过头,来处理add_op<>中存在的组合爆炸。对于每一对可以直接或间接相加的类型,都需要做一个add_op<>的特化版本。这当然不够好。我们可以进一步抽象add_op,然后加以优化。我把整个add_op<>模板改写成如下代码:
template<typename VT1, typename VT2>
variant add_op(const variant& v1, const variant& v2) {
typedef type_ret<VT1, VT2>::type RetT;
return variant(v1.operator RetT()+v2.operator RetT());
}
这里,我首先利用type_ret模板(模板元函数)获得两个操作数相加后应有的返回类型。这个模板一会说明。然后,调用variant上的类型转换操作符,将两个操作数转换成返回类型。最后相加,并创建返回variant对象。代码非常简单,没法再简单了。
再来看看type_ret<>:
template<typename T1, typename T2>
struct type_ret
{
typedef T1 type;
};
template<>
struct type_ret<int, double>
{
typedef double type;
};
template<>
struct type_ret<string, double>
{
typedef double type;
};
… //其他类型对的返回类型
type_ret<>是典型的模板元函数,没有任何实际代码,只有编译时计算的typedef。主模板将第一个类型参数typedef出一个别名。其后的模板特化对于一些特殊的情况做出定义,如int和double相加返回第二个操作数类型double(即所谓的类型提升)。
我们现在已经优化了variant+varint的代码。现在来看看如何优化variant类型和其他类型的加法:
template<typename T>
variant operator+(const variant& v1, const T& v2) {
return v1+variant(v2);
}
template<typename T>
variant operator+(const T& v1, const variant& v2) {
return variant(v1)+v2;
}
这非常简单,直接利用了variant+variant,将其它类型的操作数转换成variant类型,然后相加。
...全文
1398 33 打赏 收藏 转发到动态 举报
写回复
用AI写文章
33 条回复
切换为时间正序
请发表友善的回复…
发表回复
WuBill 2008-06-14
  • 打赏
  • 举报
回复
不是很懂, mark,有时间学习一下
X6C4_1996 2008-06-14
  • 打赏
  • 举报
回复
[Quote=引用楼主 longshanks 的帖子:]
优化variant实现
让我们先从操作数都是variant的二元操作符入手:
variant operator+( const variant& v1, const variant& v2) {…}

简单起见,先考察operator+的实现,然后扩展到其他操作符。
由于操作数是variant类型,那么它们可能代表不同的类型。我们必须知道操作数的实际类型,才能对其实施相应的+操作。最传统的办法就是使用switch:
variant operator+(const variant& v1, const variant& v2) {
switch(v1.get_type_code())
{
case vt_double:
switch(v2.get_type_code())
{
case vt_double:
…;
break;

}
case vt_int:
switch(v2.get_type_code())
{
case vt_double:
…;
break;

}

}
}
[/Quote]


不需要这么多,可以写一个函数CheckType(variant,variant,variant&,variant&),先判断类型是否匹配,如果不匹配,只要有一个是double,就把另外一个转换成double......
gunsand 2007-07-12
  • 打赏
  • 举报
回复
LZ还是比较热情的.. 是个小牛人 .
lgdgyd2 2007-07-11
  • 打赏
  • 举报
回复
up mark
nevergone 2007-07-11
  • 打赏
  • 举报
回复
看不懂
qingcairousi 2007-07-11
  • 打赏
  • 举报
回复
嗯……就我亲身体验而言,TMP带来的代码增长还不是可以忽略不计的,前阵子写一个协议的parser,用了Spirit,结果生成的debug的程序是147MB!!!编译一次要花15分钟!!要知道这个协议远不能算复杂的协议,而且也只是实现了一些简单的功能。有趣的是这个debug程序用winrar压缩以后大概只有1mb多。
当然经过优化的最终代码大概只有1MB多,但还是太大了。尤其是编译时间的大幅度延长让人受不了。
totoorange 2007-07-09
  • 打赏
  • 举报
回复
为C++版里频繁出现的精华帖子和妙语连珠感到高兴,学习+致敬!
xue_weijian 2007-07-09
  • 打赏
  • 举报
回复
高手,真是佩服!!!!!!
iambic 2007-07-09
  • 打赏
  • 举报
回复
呵呵,我比较浮躁,长篇的东西只有看印刷品才看得下去,帖子甚至电子书都读起来废尽。对楼主和楼上几位表示敬意。
tmhlcwp 2007-07-09
  • 打赏
  • 举报
回复
越来越晕~。~
庄鱼 2007-07-02
  • 打赏
  • 举报
回复
是啊,以前作项目的时候,一看到有人用模板(确切的说是定义模板类或是模板函数),就发回去要他重写,否则就必须写一个书面的理由,如果其选择的是后者,那就召集项目组的成员开会讨论有无替代方案,如果没有,再讨论模板中需要处理的问题、注意事项,一一记录下来,叫其做成文档随模板一起备案,而且规定模板一旦完成并形成文档就决不允许更改,除非另写一个替代模板。
这些都与担心泛模板化导致系统开发后期成本的增加。
因此,有时不免会反其道,倾向于基于函数的编程,甚至,有些东西干脆就要求采用函数式编程。就因为这样,有时通常会追求那种增加中间变量减少类、函数复杂度的方法,这也是我在楼主另一篇文章中建议避免对variant进行泛化扩充的原因。
longshanks 2007-07-02
  • 打赏
  • 举报
回复
我刚开始写这个案例的测试代码时,就曾经把编译器搞崩溃了。因为递归结束条件不对。vc2005很礼貌地吐出一句:“fatal error C1202: recursive type or function dependency context too complex”。
longshanks 2007-07-02
  • 打赏
  • 举报
回复
凡事有利有弊。GP和TMP是非常尖端的编程技术,就像东方明珠电视塔的塔尖那么尖。多数开发人员无法很快掌握,但技术总是在发展和推广的。就好比17世纪,微积分也仅仅是牛顿和莱布尼兹之类的牛人才能玩得转。但不到两个世纪,微积分差不多就成了大学生的课程。同样的情况,在编程界更明显。80年代的程序员们对于OOP往往带着抵触的情绪,而如今,大多数程序员似乎没有对象就活不成。
我一直认为C++的特性不是所有人都该玩的,就像“学习和使用”一帖中所说的那样。象本帖中的GP和TMP运用自然不是很多新手能够控制的。但是,毕竟还是有人能够使用这些高级特性的。如果有人能够使用,却不去使用,无疑是莫大的浪费。关键在于两个方面:第一,库,把这类高级技术运用在库中,而不是在直接实现应用的代码中。而库是可以集中开发的,由专人,能够驾驭这些高级技术的人开发。其次,文档,首当其冲是使用文档,应当详尽而完整。其次是开发的设计说明,应充分阐述其中原理。
GP和TMP运用带来的直接好处便是代码量下降、简洁、更易于维护(当然是针对懂行的人而言)。这些技术实际上可以使得过去大量分离,并且冗余的代码,以库或组件的形式集约化。目前大多数企业都没有注意到这点,毕竟这些技术太新。企业需要相当的时间加以消化,就像消化OOP那样。但是,对于生产率和利润的追求,终究会促使企业选择这些阳春白雪。
至于模板的开销,首先,GP和TMP有助于消除运行时开销,(uBLAS之类的库已经可以达到与Fortran比肩的程度);其次,对于动辄数百M的内存容量,模板造成的代码增长几乎可以忽略不计。本帖中的案例在使用GP和TMP后,代码量反而比使用switch少,模板对于代码量并不总是有负面的影响;最后,GP和TMP的确使编译速度减慢很多,但对于现有的强劲的硬件系统而言,也不足以构成反对的理由。(正因为如此,C++标准委员会一改原先对于影响编译速度的技术的抵触态度,决定将编译速度不作为考量语言新特性的标准)。
至于模板的编译可靠性,不用担心,boost库的那帮疯子已经对现有编译器施加了足够的压力,不会有什么问题的。
调式对于GP,特别是TMP而言,是推广过程中最主要的拦路虎。GP随着C++09的出台,其调试的问题将会极大的缓解。而TMP,也会有不小的进步。
我的那个错误并不是GP和TMP的缺陷,如果我使用普通函数,也使用了不正确的递归结束条件,编译自然没有问题,但运行时肯定会出洋相。如果让我选择,我会希望编译时的错误,我最害怕的,就是在客户面前洋洋得意地展示一个漂亮功能时,跳出一个难看的运行时错误。
最后一个重要的因素,技术在实践中的运用通常都会滞后。所以,尽管GP和TMP尚不完善,难以理解,但并不影响对其的推广和应用尝试。当人们逐步认识和熟悉了这些技术后,相应的技术也就成熟了。就像那个歌里唱的:
“--葡萄成熟还早的很,现在上来干什么?
--...等我爬上它就成——熟——了——。”
庄鱼 2007-06-30
  • 打赏
  • 举报
回复
唉!楼注终于谈到这有趣而又接近“雷区”的话题了,每每遇到这种问题,编译器都会长考,真担心哪天写的东西让机器就此挂起来*_*#
有关类型识别与匹配,区分简单类型与复杂类型,这在C++模板应用中是一个专门的话题,目前的模板还不支持字符串与类型匹配,否则的话,C++的地位真的很难撼覆了。
lidongri 2007-06-30
  • 打赏
  • 举报
回复
TMP,呵呵,好东西
variant则不太了解
帮顶后再看
奶糖人五号 2007-06-30
  • 打赏
  • 举报
回复
又见牛帖,mark,再次膜拜
FingerStyle 2007-06-30
  • 打赏
  • 举报
回复
顶起来。。。。
Vitin 2007-06-29
  • 打赏
  • 举报
回复
LZ的话让人心动,看来是需要好好研究一下TMP了,呵呵。

学习。
longshanks 2007-06-29
  • 打赏
  • 举报
回复
至于TMP,使用起来也没那么深奥(当然,实现起来是颇费脑力的)。boost::mpl做得非常好。它不象loki那样特立独行,而是将STL中的诸多概念照搬过来,形成了同STL类似的体系。所不同的仅仅是STL计算的是数据对象,而boost::mpl计算的是类型。
所以,mpl的容器也有容器,包括mpl::vector,mpl::deque,mpl::list,mpl::map等等,命名上几乎和STL一样。mpl也有迭代器,有一些算法负责操作迭代器,如我帖子里用到的mpl::advance,语义同STL的advance一样。当然,mpl也有算法,如mpl::count,mpl::find_if等等。此外,还有inserter之类的辅助类型和算法。
mpl中提出了“元函数”的概念。一般的函数通常是:
double f(int, double);
而元函数,实际上是一类特殊的类模板:
template<typename T>
struct f //用class也可以,但struct方便些,无需public:
{
typedef T* ptr_type; //通常只有typedef或static const等编译时内容,没有的成员
typedef T& ptr_type;
}
元函数用起来差不多是这样:
typedef f<int>::type ptr_int; //ptr_int就是int*
而普通函数用起来:
double a=f(10, 2.3);
两者颇为相似。元函数的typedef相当于普通函数的赋值(=)。而元函数内的public typedef可以看作“返回值”。(请注意,元函数可以有不止一个“返回值”)。元函数没有循环,但可以递归。(所以,TMP实际上可以看作一种函数式编程语言)。而分支语言则使用模板的特化实现。
一旦习惯了元函数那种古怪的风格,那么使用mpl则是一件轻而易举的事,特别是对于熟悉STL的人:
typedef mpl::vector<int, double, string, bool> my_types;
typedef find<my_types, double>::type db_type; //算法的返回::type是一个iterator
typedef begin<my_types>::type first; //mpl的容器不可能有成员,所以begin是独立元函数
const db_id=distance<first, db_types>::value; //获得double在my_type中的序号,是1
总的来说,实现mpl是恐怖的事。但使用起来还是惬意的。:)
healer_kx 2007-06-29
  • 打赏
  • 举报
回复
最近C版好贴频出,这就是其中一篇,
向楼主致敬!
加载更多回复(11)

64,646

社区成员

发帖
与我相关
我的任务
社区描述
C++ 语言相关问题讨论,技术干货分享,前沿动态等
c++ 技术论坛(原bbs)
社区管理员
  • C++ 语言社区
  • encoderlee
  • paschen
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
  1. 请不要发布与C++技术无关的贴子
  2. 请不要发布与技术无关的招聘、广告的帖子
  3. 请尽可能的描述清楚你的问题,如果涉及到代码请尽可能的格式化一下

试试用AI创作助手写篇文章吧