一、函数对象(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); }
二、函数对象的使用场景
- 函数对象可以运用在算法、容器中,也可以单独使用
- 并且标准库还预先定义好了一些函数对象,在下一篇文章介绍:https://blog.csdn.net/qq_41453285/article/details/105486301
三、函数对象相比函数的优点
①函数对象是一种带状态的函数
- “行为像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