适配器adapter与仿函数functor是C++ 标准库中提供的部件,可以将STL提供的一些基本算法(比如sort,count等等)为我们实际的项目场景所用。
本文参考侯捷老师的STL课程,分析一个仿函数bind2nd,来深入理解适配器和仿函数。
什么是仿函数
仿函数本质是一个重载了operator()的类。在代码层面上,调用函数时就是在函数名称后加一对小括号,再在小括号中加入实参即可,比如func(var1,var2)。而一个重载了operator()的类,也可以这样使用。比如类名为A,其对象为a,可以写出a(var1,var2)这样的代码来实现同样的功能。
下面这个例子使用标准库的sort函数对一些石头变量进行排序,sort函数的第三个参数是比较法则。请看最后两句代码,这里采用了两种实现,第一种是直接使用一个函数名作为参数,第二种则是使用重载了operator()的类的临时对象。二者可以实现相同的功能。因此这种重载operator()的类或对象就叫做仿函数。
#include<vector>
#include<algorithm> //包含标准库的sort算法
using namespace std;
//定义一个石头,有一个weight属性
struct stone {
double _weight;
stone(double tmp) { _weight = tmp; };
};
//用于比较重量的模板函数
template<typename T>
bool myCmpFunc(const T& a, const T& b) {
return a._weight < b._weight;
}
//模板类,重载了operator()
template<typename T>
struct myClass {
bool operator()(const T& a, const T& b) {
return a._weight < b._weight;
}
};
int main()
{
vector<stone> vecA;//存储石头们
for (int i : {5, 4, 3, 2, 1}) {
vecA.push_back(stone(i));
}
//1. 第三个参数是一个函数名
sort(vecA.begin(), vecA.end(), myCmpFunc<stone>);
//2. 第三个参数是 myClass类的一个临时对象,也就是仿函数
sort(vecA.begin(), vecA.end(),myClass<stone>());
}
符合STL标准的仿函数
对于STL,仿函数是我们最容易改写的也几乎是唯一能改写的组件了(其他的组件比如容器,分配器等很少涉及对他们的改写)。
而为了使我们写的仿函数能完美适配到STL,需要让其继承STL提供的几个标准接口,比如unary_function, binary_function。二者分别表征一个操作数的函数和两个操作数函数。
也就是说我们上一段的仿函数的标准写法是这样,其中binary_function的三个模板参数表示函数的第一个参数,第二个参数和函数返回值的类型。
//模板类,重载了operator()
template<typename T>
struct myClass:public binary_function<T,T,bool> {
bool operator()(const T& a, const T& b) {
return a._weight < b._weight;
}
};
下面贴一下模板类binary_function的标准库实现,可以看到仅仅是进行了一些typedef,而正是这些typedef可能会被STL的一些算法用到,下面会有这些typedef被调用的例子,所以我们写的仿函数需要继承他们。
// STRUCT TEMPLATE binary_function
template <class _Arg1, class _Arg2, class _Result>
struct binary_function { // base class for binary functions
using first_argument_type = _Arg1;
using second_argument_type = _Arg2;
using result_type = _Result;
};
#endif // _HAS_AUTO_PTR_ETC
bind2nd的例子来看仿函数和适配器
适配器是一个比较抽象的概念,没有明确的定义,可以理解成一个中介的作用,或者说一个转接线的概念。
首先我们来看下面一小段代码,count_if是标准库提供的一个算法,算法的第三个参数是比较法则。less<int>()很明显是一个模板类的对象,是标准库的一个仿函数,使用时有两个参数,功能是判断它的第一个参数是否小于第二个参数。
#include<vector>
#include<algorithm> //包含标准库的count_if算法
#include<functional> //包含bind2nd函数
#include<iostream>
...
vector<int> v;
cout << count_if(v.begin(), v.end(), bind2nd(less<int>(), 40));
...
bind2nd(less<int>(), 40)的功能是将less函数的第二个参数绑定为40,这里他可以当做算法count_if与函数less的中介,就是说它表现为一个函数适配器的形象。同时,由于它封装了less函数,并且在使用时也表现得像一个函数,那它也可以理解成是一个仿函数,也因此,它被定义在<functional>头文件中。
下面来看一下bind2nd的实现方法,可以明显看出它仿函数的特性。(下图截自侯捷老师的STL课程)
bind2nd.PNG 可以看到bind2nd是个模板函数,其浅灰色字体的内容正是上文提到的binary_function中的typedef,这表示其第一个模板参数Operation应该是一个仿函数类型(在这里实际上就是less<int>这个类型),这也证明了我们在自己写仿函数时需要继承STL的接口。
从格式上看,其返回值类型是一个模板类binder2nd<Operation>的对象,且构造函数的参数正是模板函数bind2nd的参数,也就是说这个函数仅仅是做了一个封装,真正的功能实现在这个模板类中,下图就展示了模板类binder2nd<Operation>的实现。
可以看到binder2nd<Operation>是一个模板类,并且它重载了operator()且继承了unary_function,也就是它表现为一个单参数的仿函数。
它的构造函数对Operation less 和int型的40进行记录,并在重载的operator()中将less的第二个参数绑定为40,实现了全部的要求。
再问一个题外话,为什么在binder2nd这个模板类外面再包装一个模板函数bind2nd呢?
这是因为类模板使用时要自己写明模板参数的类型(这里就是less<int>类型),这一点对程序员而言是很困扰的。而模板函数不用写,编译的时候会自动进行实参推导。
另外,新版本的STL将bind2nd,bind1st等函数用一个实现上更复杂的统一的bind()函数来封装,但本质上一样的,且目前的版本bind2nd,bind1st等函数也可以正常使用。
网友评论