C++(标准库):31---STL函数对象之(函数对象的概念及使用)

一、函数对象(Function Object)概述

  • 仿函数(functors)是早期的命名,C++标准规则定案后所采用的的新名称是函数对象(function objects)
  • 所谓函数对象,就是一个定义了operator()的对象

函数对象就是一个“行为类似函数”的对象

  • 函数的调用需要使用小括号进行调用。为了能够达到“行为类似函数”的目的,函数对象必须自定义(或者说重载、改写)function call运算子(operator())
  • 拥有这样的运算子后,我们就可以在仿函数的 对象后面加上一对小括号以此来调用函数对象所定义的operator()
  • 例如下面是一个函数调用:
void function(int x, int y);

int main()
{
    //函数的调用
    function(1, 2);
}
  • 例如下面是一些函数对象的调用:
class X
{
public:
    void operator()(int x, int y);
};

int main()
{
    X fo;
    //函数对象的调用
    fo(1, 2); //等价于fo.operator();
}
class Y
{
public:
    void operator()();
};

int main()
{
    Y po;
    //函数对象的调用
    po(); //等价于po.operator()
}
  • 如果类含有构造函数,那么使用函数对象前需要先使用构造函数构造对象。例如:
class Z
{
private:
    int value;
public:
    Z(int initialize) :value(initialize) {}
    void operator()(int elem);
};

int main()
{
    //先Z(10)构造一个Z对象,然后再(3)调用其内部的operator()
    Z(10)(3);
}

二、函数对象的使用场景

三、函数对象相比函数的优点

①函数对象是一种带状态的函数

  • “行为像pointer”的对象我们称之为智能指针,同理,“行为像function”的对象我们称之为函数对象
  • 函数对象的能力超越了operator。函数对象可拥有成员函数和成员变量,这意味着函数对象拥有状态:
    • 事实上,在同一时间点,相同类型的两个不同的函数对象所表述的相同机能,可具备不同的状态。这在寻常函数是不可能的
    • 另一个好处是,你可以在运行期初始化它们——当然必须在它们被使用(被调用)之前
  • 演示案例:如果我们需要将vector内的每个元素都加上特定的值。如果不使用函数对象,而使用函数模板,那么代码如下,这个方案的主要缺点是:
    • 针对于每个函数模板的调用,我们需要为其每一份实例都生成一个实例化,因此下面为生成两份add()函数的实例定义
    • 这种方法很不好,因为如果以后调用其他版本的add()函数,那么还需要生成其他版本的实例化,这样的话代码就十分的冗余
template<int theValue>
void add(int& elem)
{
    elem += theValue;
}

int main()
{
    vector<int> coll{ 1,2,3,4,5,6,7,8 };

    //如果是每次加上10,那么需要调用这个模板
    for_each(coll.begin(), coll.end(), add<10>);

    //如果是每次加上20,那么需要调用这个模板
    for_each(coll.begin(), coll.end(), add<20>);
}
  • 演示案例:如果改用函数对象,那么就方便很多。相比于函数的优点如下:
    • for_each()每次调用时都会创建一个临时函数对象,这些对象都有自己的状态,但是它们都是由同一种类型定义而来,代码不会冗余
class AddValue
{
private:
    int theValue;
public:
    AddValue(int v) :theValue(v) {}
    void operator()(int &elem)const
    {
        elem += theValue;
    }
};

int main()
{
    vector<int> coll{ 1,2,3,4,5,6,7,8 };

    //创建一个AddValue临时对象给for_each,临时对象的theValue=10
    //每次调用临时对象.operator(int &elem)
    for_each(coll.begin(), coll.end(), AddValue(10));

    创建一个AddValue临时对象给for_each,临时对象的theValue=20
    for_each(coll.begin(), coll.end(), AddValue(20));
}
  • 关于函数对象的内部状态,在下面还有演示案例 

②每个函数对象有其自己的类型

  • 普通函数,唯有在其签名式不同时,才算类型不同。而函数对象即使签名式相同,也可以有不同的类型
  • 事实上由函数对象定义的每一个函数行为都有其自己的类型。这对于“运用template实现泛型编程”乃是一个卓越的贡献,因为这么一来我们便可以将函数行为当做template参数来运用。这使得不同类型的容器可以使用同类型的函数对象作为排序准则。也可确保你不会在“排序准则不同”的集合间赋值、合并或比较
  • 你甚至可以设计函数对象的继承体系,以此完成某些特别事情,例如在一个总体原则下确立某些特殊情况

③函数对象通常比寻常函数速度快

  • 就template而言,由于更多细节在编译器就已经确定,所以畅通可能进行更好的优化。所以,传入一个函数对象(而非寻常函数)可能获得更好的执行效能

四、演示案例(将函数对象作为容器的排序准则)

使用预定义的函数对象

  • set容器在创建时,如果不指定参数2,那么set容器采用默认的排序方法(升序)对容器内的元素进行排序。例如:
//默认采用系统提供的方式对set内的元素进行排序
set<int> _set{ 0,3,1,4,2,5 };
//其等价于set<int, std::less<int>> _set{ 0,3,1,4,2,5 };

for (const auto& val : _set)
{
    std::cout << val << " ";
}
std::cout << std::endl;
  • 运行结果如图所示:

  • 如果我们创建set时,为其参数2指定一个函数对象,让其对其中的元素进行降序排序
    • 其中std::greater是系统预定义的函数对象,在后面一篇文章介绍
//指定set的参数2,以std::greater函数对象为基准,对set进行降序排序
set<int, std::greater<int>> _set{ 0,3,1,4,2,5 };

for (const auto& val : _set)
{
    std::cout << val << " ";
}
std::cout << std::endl;
  • 运行结果如图所示: 

使用自定义的函数对象

  • 例如下面有一个Person类,其存储我们的数据。另外定义一个PersonSortCriterion类,其能够创建函数对象,并且可以对Person进行排序
  • 代码如下:
class Person
{
public:
    std::string firstname()const { return _firstName; }
    std::string lastname()const { return _lastName; }
private:
    std::string _firstName;
    std::string _lastName;
};

class PersonSortCriterion
{
public:
    bool operator()(const Person&lhs, const Person& rhs)const
    {
        return (
            (lhs.firstname() < rhs.firstname()) ||
            (lhs.firstname() == rhs.firstname() && lhs.lastname() < rhs.lastname())
            );
    }
};
int main()
{
    //采用set默认的排序方式对其中的Person对象进行排序
    set<Person> coll1;

    //采用PersonSortCriterion的排序方式对其中的Person对象进行排序
    set<Person, PersonSortCriterion> coll2;
}
  • coll2那个set,其在内部会每次调用两个Person对象,然后调用PersonSortCriterion.operator()运算符比较两个Person对象,然后将其保存到set容器中

五、函数对象拥有内部状态

  • 下面展示function object如何能够“行为像个函数同时又拥有多个状态”:

演示案例①

class IntSequence
{
private:
    int value;
public:
    IntSequence(int initialValue) :value(initialValue) {}
    int operator()()
    {
        return ++value;
    }
};

int main()
{
    vector<int> coll;

    //从coll.begin()开始插入9个元素
    generate_n(back_inserter(coll), 9, IntSequence(1));
    for (const auto& elem : coll)
    {
        std::cout << elem << " ";
    }
    std::cout << std::endl;

    //向[begin+1,end-1)区间内插入元素
    generate(next(coll.begin()), prev(coll.end()), IntSequence(42));
    for (const auto& elem : coll)
    {
        std::cout << elem << " ";
    }
}
  • generate_n():调用参数3产生新值,并将新值赋值给以参数1起始的区间内的前参数2个元素
  • generate():调用参数3产生新值,并将新值赋值给[参数1,参数2)所在区间内的元素
  • 程序运行结果如下图所示:

  • 上面的演示案例①调用的函数对象是by value传递给算法的:
    • 优点是:你可以传递常量表达式或暂态表达式
    • 缺点是:你无法改变function object的状态,因为是by value传递的,所以每次传递给算法时,算法操作结束之后,function object的状态仍与传入算法前一致(不论该function object是外部创建的,还是算法临时创建的)
  • 有三个办法可以从“运用了function object”的算法中获得结果:
    • ①在外部持有状态,并让function object指向它
    • ②以by reference方式传递function object(见下面演示案例②)
    • ③利用for_each()算法的返回值(下面“六”介绍)

演示案例②(by reference方式传递function object)

  • 为了以by reference方式传递function object,你需要在调用算法时明示function object是个reference类型
  • 代码如下:

 

  • 运行结果如下所示:

 

  • 原因解释:
    • 第一次调用generate_n()时function object seq是以by reference方式传递
    • 第二次调用generate_n()时是创建一个临时对象,并且在seq尾后进行插入4个元素,因此与seq无关
    • 第三次调用generate_n()时是by value方式传递seq,因此seq的状态没有改变(其内部的value没有增加,还是为6)
    • 第四次调用generate_n()时,因为第三次调用seq的状态没有改变(其内部的value没有增加,还是为6),所以还是从6开始插入

六、for_each的返回值

  • 在“五”中我们介绍过,如果想要算法改变function object的状态,那么有三种方法,其中一种是for_each()
  • for_each()语法参阅:https://blog.csdn.net/qq_41453285/article/details/105486526
  • 使用for_each()算法,就不必费神以by reference方式传递function object,因为for_each()算法会返回参数3(它已在算法内部被改动过)的一个拷贝(副本)

演示案例

class MeanValue
{
private:
    long num;
    long sum;
public:
    MeanValue() :num(0), sum(0) {}
    void operator()(int elem)
    {
        num++;
        sum += elem;
    }
    double value() const{
        return static_cast<double>(sum) / static_cast<double>(num);
    }
};

int main()
{
    vector<int> coll{ 1,2,3,4,5,6,7,8 };

    MeanValue mv = for_each(coll.begin(), coll.end(), MeanValue());
    std::cout << "mean value: " << mv.value();
}
  • 运行结果如下图所示: 

  • 我们将MeanValue()临时对象传递给for_each(),整个算法执行过程中都是用这个临时对象的operator(),最后将MeanValue临时对象进行返回
  • 这个演示案例在“for_each()”一文中也介绍过,稍有不同,但是原理一致:https://blog.csdn.net/qq_41453285/article/details/105486526
  • 也可以使用lambda完成任务(详情见文章:https://blog.csdn.net/qq_41453285/article/details/105486309),并以by reference方式传递返回值。然而在这种情形下lambda不见得比较好,因为function object比较方便,例如当我们需要为associative或unordered容器声明一个hash函数或一个排序准则或相等准则。Function object通常是全局性的,这一事实有利于我们把它放入头文件或程序库,而lambda则是方便局部性地指明行为

七、Predicate(判别式)与函数对象

  • 待续,详情见《C++标准库》P483
  • 6
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

董哥的黑板报

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值