美文网首页
从bind2nd函数看懂C++ STL的适配器与仿函数

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

作者: 石小鑫 | 来源:发表于2022-01-04 19:42 被阅读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等函数也可以正常使用。

    相关文章

      网友评论

          本文标题:从bind2nd函数看懂C++ STL的适配器与仿函数

          本文链接:https://www.haomeiwen.com/subject/uemmzltx.html