被误解的C++——优化variant实现
优化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类型,然后相加。