0%

C++中tuple的偏操作

这篇文章的名字可能不太好理解,因为我也不太能很好的给这个操作起个名字,所以就模仿数学中的“偏”给它叫作偏操作了。

问题起因

在刷题过程中,我们或许会遇到一些要把数据聚合在一起的情况,也就是使用struct或者tuple的情况。如果仅仅只是保存数据,我个人是习惯用tuple,人比较懒,这样可以少起些名字而且还自带了重载比较运算符。不过默认重载的比较运算符是一个元素一个元素的比较的,很多时候并不好用,比如如果我们只想以某一个位置的元素为关键字比较,假设有一组二元组V,要以第1个下标为关键字排序,可能我们就要这样写

1
std::sort(V.begin(),V.end(),[](auto&a,auto&b){return std::get<1>(a)<std::get<1>(b);});

那个lambda看起来是不是又长又蠢,实现的功能很简单,但是格式非常冗长,而且你都已经用tuple了,一个程序里面不可能只需要一次这样的操作,你可能在后面还要写无数个这样简单又冗长的lambda,像我这样的懒人是不能接受的。

使用模板

模板是C++的精髓,如果会使用模板,就能使代码变得简单而优雅。不过我也只是在网上看过一点模板的内容,对模板并不了解,所以后面的代码可能存在很多我也不清楚的隐患,所以如果要使用需谨慎。

我们要做的事情很简单,用一句话说就是,对两个tuple的某个位置的元素进行二元操作。这么看我们需要的或许就是一个模板,传入类型T、位置N和操作OP,然后能够实例化出一个函数对象,之后经过我各种查资料和模仿,得到了最初版本,

1
2
3
4
template<typename T,int N,class OP>
struct T_OP{
auto operator()(T&a,T&b){return OP{}(get<N>(a),get<N>(b));}
};

我给它命名为T_OP,T可以理解为tuple的缩写,OP是操作的缩写,合在一起就是对tuple进行操作,也可以说是TOP!

T_OP是一个结构体,重载了括号运算符,也叫函数对象。在重载括号运算符时返回对两个tuple指定元素调用OP后的结果,返回类型自动推断就可以了。

按照之前的例子简单的测试一下:

想象中的一样,按照下标1的元素进行排序了,好了收工,之前还有一点小问题。

tuple和单值操作

最初版本的T_OP对于两个tuple的操作已经实现了,但是试想如果我们要在lower_bound中使用,找到下标1大于等于4的元素,如果按照最初的版本可能会写成这样

1
auto it=lower_bound(V.begin(),V.end(),{0,4},T_OP<T,1,less<int>>{});

这样看着也很蠢,我们需要的只是一个4但是却要写了个大括号把其他值都写了。所以我们需要的是一个值能和tuple比较的函数,继续对括号运算符进行重载。

1
2
3
4
5
template<typename T,int N,class OP>
struct T_OP{
auto operator()(T&a,T&b){return OP{}(get<N>(a),get<N>(b));}
auto operator()(T&a,tuple_element_t<N,T>b){return OP{}(get<N>(a),b);}
};

tuple_element_t<N,T>T的第N个下标的类型通过这种方法能够重载出tuple和单值操作的版本的函数。注意参数的顺序,后面是在经历了各种尝试之后得出的完整版本。

1
2
3
4
5
6
7
template<typename T,int N,class OP>
struct T_OP{
auto operator()(T&a,T&b){return OP{}(get<N>(a),get<N>(b));}
auto operator()(tuple_element_t<N,T>a,T&b){return OP{}(a,get<N>(b));}
auto operator()(T&a,tuple_element_t<N,T>b){return OP{}(get<N>(a),b);}
auto operator()(tuple_element_t<N,T>a,tuple_element_t<N,T>b){return OP{}(a,b);}
};

至于为什么还要重载两个参数都是单值的版本的,refuce函数是并行的,它会并行计算部分结果之后再将各部分结果相加,假设是并行的对int求和,最后合并的时候肯定会出现两个int相加的情况。不过准确来说,如果要用于reduce,第四个重载的参数类型也不是第N个下标的类型,应该是OP的返回类型。

结尾

上面模板里面虽然写了四个重载,但是实际编译出来只会对用到的函数进行实例化,如果没有用到是不会实例化的,这个可以用gdbinfo functions查看实例化的函数。

最后按照惯例上个实战截图,证明这个东西真的能用