从bind2nd函数看懂C++ STL的适配器与仿函数

2022-01-04  本文已影响0人  石小鑫

  适配器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.PNG

  可以看到binder2nd<Operation>是一个模板类,并且它重载了operator()且继承了unary_function,也就是它表现为一个单参数的仿函数。
  它的构造函数对Operation less int型的40进行记录,并在重载的operator()中将less的第二个参数绑定为40,实现了全部的要求。

  再问一个题外话,为什么在binder2nd这个模板类外面再包装一个模板函数bind2nd呢?
  这是因为类模板使用时要自己写明模板参数的类型(这里就是less<int>类型),这一点对程序员而言是很困扰的。而模板函数不用写,编译的时候会自动进行实参推导

  另外,新版本的STL将bind2nd,bind1st等函数用一个实现上更复杂的统一的bind()函数来封装,但本质上一样的,且目前的版本bind2nd,bind1st等函数也可以正常使用。

上一篇下一篇

猜你喜欢

热点阅读