一、函数对象(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;

  • 运行结果如图所示:

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

  • 如果我们创建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;

  • 运行结果如图所示:

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



使用自定义的函数对象

  • 例如下面有一个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)所在区间内的元素
  • 程序运行结果如下图所示:

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


  • 上面的演示案例①调用的函数对象是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()语法参阅
  • 使用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();
}

  • 运行结果如下图所示: 

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

  • 我们将MeanValue()临时对象传递给for_each(),整个算法执行过程中都是用这个临时对象的operator(),最后将MeanValue临时对象进行返回


  • 这个演示案例在“for_each()”一文中也介绍过,稍有不同,但是原理一致​
  • 也可以使用lambda完成任务,并以by reference方式传递返回值。然而在这种情形下lambda不见得比较好,因为function object比较方便,例如当我们需要为associative或unordered容器声明一个hash函数或一个排序准则或相等准则。Function object通常是全局性的,这一事实有利于我们把它放入头文件或程序库,而lambda则是方便局部性地指明行为

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

  • 待续,详情见《C++标准库》P483