zoukankan      html  css  js  c++  java
  • C++ 仿函数和适配器

    本文从不断复杂的应用场景入手,来说明C++设计仿函数和适配器的原因,并深入源码来介绍仿函数和适配器的使用方法。

    仿函数

    现有一个vector,需要统计大于8的元素个数。
    使用std::count_if,代码可能长这个样子。

    bool greaterThan8(int n){
    	return n > 8;
    }
    int count = count_if(vec.begin(), vec.end(), greaterThan8);
    

    count_if第三个参数接收的是一个函数指针,并且这个函数指针只接收一个参数,返回值是bool。
    count_if是将参数1和参数2之间的每一项都会作为参数去调用第三个参数,然后返回第三个参数每一次true的次数。

    这里是将8直接写在这里,并不符合编程规则,也不容易维护。

    或者新加个参数

    bool greaterThanN(int n, int x){
    	return n > x;
    }
    

    看起来可以解决问题。
    但是上文说了,count_if的第三个参数函数指针,只能接收一个参数。

    如何取消这个参数呢?
    1、全局变量

    int minValue = 8;
    bool greaterThanN(int n){
    	return n > minValue ;
    }
    

    能解决问题,但是并不优雅。
    1、容易出错
      为什么这么说呢,我们必须先初始化maxLength的值,才能继续接下来的工作,如果我们忘了,则结果未定义。
    此外,变量maxLength和函数LengthIsLessThan之间是没有必然联系的,编译器无法确定在调用该函数前是否将变量初始化。
    需要使用者自己控制,则增加维护成本和出错可能性。

    2、没有可扩展性
      如果我们每遇到一个类似的问题就新建一个全局变量,尤其是多人合作写代码时,很容易引起命名空间污染(namespace polution)的问题。

    3、全局变量的问题
      全局变量花销很大,一般是编程过程中极力避免的

    #define VALUE 8
    bool greaterThanN(int n){
    	return n > VALUE;
    }
    

    换成宏是节省了全局变量的代价。但是依然不够优雅,容易出错且可扩展性差。

    3.成员变量
    我们既想要它既能想普通函数一样传入给定数量的参数,还能存储或者处理更多我们需要的有用信息。
    如果存成成员变量,那成员函数调用不就是很正常的事了吗?

    对,为了实现上面的内容,C++语言引入了仿函数的概念。
    仿函数(functor),其实是像函数一样使用的对象,也被称为函数对象(function object)。仿函数里面重载了"()"操作符,是我们可以像调用函数一样调用它。

    仿函数的基本使用

    class functor {
    public:
            functor(int x) { value =x; }
    	void operator()(const int& x) { cout << x+value << endl;}
    private:
            int value;
    };
    
    functor fun(10);
    fun(5); //15
    

    为了满足文初的应用场景,使用仿函数的代码,大概长这样

    template<typename T>
    class greater{
    public:
    	greater(const T&x) { info = x; }
    	bool operator()(const T& x) { return info > x; }
    private:
    	T info;
    };
    

    调用代码

    greater<int> ge(20);
    int count = count_if(vec.begin(), vec.end(), ge);
    

    或者直接使用临时变量

    int count = count_if(vec.begin(), vec.end(), greater<int>(20));
    

    当然,这里可以不用模板,但是为了扩展和方便,建议还是养成用模板的习惯。

    另外,不用普通函数指针是因为函数指针的局限:

    1. 不能满足STL的抽象要求
      参数,返回值如何设置
    2. 无法和STL其他组件交互
    template<typename _Kty, typename _Pr = less<_Kty>,...>
    class set{...};
    
    class Person{...};
    
    std::set<Person, std::less<Person> > set1, set2; //operator<做排序行为
    std::set<Person, std::greater<Person> >set3,set4;//operator>做排序行为
    
    set1 = set2;  //正确,相同的型别
    set1 = sete3; //错误,不同的型别!
    

    STL中也预先实现了一些仿函数,需要包头文件#include<functional>

    另外在c++11里面可以通过lambda表达式解决上述问题:

    #include <vector>
    #include <iostream>
    #include <algorithm>
    #include <functional>
    using namespace std;
    int main()
    {
        std::vector<int> c{ 1, 5, 3, 4, 5, 6, 7 };
        int x = 5;
        int k=std::count_if(c.begin(), c.end(), [x](int n){return x == n; });
        cout << k << endl;
        return 0;
    }
    

    适配器

    bind1st/bind2nd

    vector<int> vec{0,0,0,10};
    

    怎么找到第一个非0元素?
    通过std::not_equal_to,代码应该是下面这样

    std::vector<int>::iterator it = std::find_if(vec.begin(), vec.end(), 
      std::not_equal_to<int>(0));
    

    无法通过编译。为什么?

    • not_equal_to结构没有一个参数的构造函数
    • not_equal_tooperator()需要接受两个参数
    template<class _Ty = void>
    	struct not_equal_to
    	{	// functor for operator!=
    	typedef _Ty first_argument_type;
    	typedef _Ty second_argument_type;
    	typedef bool result_type;
    
    	_CONST_FUN bool operator()(const _Ty& _Left, const _Ty& _Right) const
    		{	// apply operator!= to operands
    		return (_Left != _Right);
    		}
    	};
    

    现在,这里需要一个插座,也就是适配器模式里的适配器。

    typename std::not_equal_to<int>::first_argument_type nonZeroItem(0);
    std::not_equal_to<int> f;
    std::vector<int>::iterator iter = find_if(vec.begin(), vec.end(),
    	std::binder1st<std::not_equal_to<int> >(f, nonZeroItem));
    

    具体调用如下

    使用f, nonZeroItem来构造std::not_equal_to<int>类型的bind1stoperator()实际上调用的是std::not_equal_to<int> operator()

    为了方便使用,std使用bind1st进行了封装

    template<class _Fn2,
    	class _Ty> inline
    	binder1st<_Fn2> bind1st(const _Fn2& _Func, const _Ty& _Left)
    	{	// return a binder1st functor adapter
    	typename _Fn2::first_argument_type _Val(_Left);
    	return (binder1st<_Fn2>(_Func, _Val));
    	}
    

    那我们的代码应该会变成这个样子

    	std::vector<int>::iterator iter = find_if(vec.begin(), vec.end(),
    		std::bind1st(std::not_equal_to<int>(), 0));
    

    binder2ndbinder1st类似。区别只在于_Func操作的是左值还是右值。
    也就是说以下两句代码的同义的

    std::vector<int>::iterator iter = find_if(vec.begin(), vec.end(),
    		std::bind1st(std::less<int>(), 0));  //std::bind2nd(std::greater<int>(), 0)
    
    std::vector<int>::iterator iter = find_if(vec.begin(), vec.end(),
    		std::bind2nd(std::greater<int>(), 0));
    

    bind1st操作的是左值,找到第一个X,使满足0 < X
    bind2nd操作的是右值,找到第一个X,使满足X > 0

    mem_fun/mem_fun_ref

    设计和上面类似,是用于适配对象的函数

    需求:打印vector中所有Person类的成员

    	std::vector<Person*> vec;
    	vec.push_back(new Person("I",1));
    	vec.push_back(new Person("love", 2));
    	vec.push_back(new Person("u", 3));
    	vec.push_back(new Person("so much", 4));
    
    	std::for_each(vec.begin(), vec.end(), &Person::Print); //编译不通过
    

    如何能在函数中调用对象的成员函数呢?
    首先,有对象obj,和函数fun,调用方法有如下几种:

    1. fun(obj)  //fun是全局函数,非成员函数
    2. obj.fun()  //obj非指针,fun是成员函数
    3. obj->fun() //obj指针对象,fun是成员函数
    

    for_each会调用_For_each
    只接收fun(obj)的调用

    所以代码应该是下面这个样子

    std::for_each(vec.begin(), vec.end(), std::mem_fun(&Person::Print));
    

    具体调用

    mem_fun_ref类似,但是用于容器中存放的不是指针的情况

    释放内存

    方法一
    	for (vector<Person*>::iterator it = vec.begin(); it != vec.end(); ++it)
    		delete (*it);
    	vec.clear();
    
    方法二
    struct DeleteItem {
    	template<typename _Ty>
    	void operator()(const _Ty p) const
    	{
    		delete (p);
    	}
    };
    std::for_each(vec.begin(), vec.end(), DeleteItem());
    vec.clear();
    
    注意

    1、有继承的情况下,容器存放指针而不是对象

    • 对象拷贝比指针拷贝对象大得多
    • 父对象的容器存放子对象时,数据会Slice

    2、使用指针记得释放内存
    3、尽量用算法代替手写循环
    代码更优雅,据说速度更快,但是还没测试过

  • 相关阅读:
    快速删除段落间多余的空行
    平时一些mysql小技巧及常识
    mysql中常用的控制流函数
    按年、季度、月分组&&计算日期和时间的函数
    Excel通过身份证获取出生年月,性别,年龄,生肖,星座,省份等信息总结归纳
    统计图表类型选择应用总结&表数据挖掘方法及应用
    EXCEL如何提取文字中包含的数字?
    一篇说尽Excel常见函数用法
    RStudio中,出现中文乱码问题的解决方案
    R-RMySQL包介绍学习
  • 原文地址:https://www.cnblogs.com/faithfu/p/8932375.html
Copyright © 2011-2022 走看看