函数对象就是一个“行为类似函数”的对象
- 函数的调用需要使用小括号进行调用。为了能够达到“行为类似函数”的目的,函数对象必须自定义(或者说重载、改写)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
void add(int& elem) { elem += theValue; } int main() { vector 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
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
_set{ 0,3,1,4,2,5 }; //其等价于set > _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
> _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
coll1; //采用PersonSortCriterion的排序方式对其中的Person对象进行排序 set coll2; }
- coll2那个set,其在内部会每次调用两个Person对象,然后调用PersonSortCriterion.operator()运算符比较两个Person对象,然后将其保存到set容器中
演示案例①
class IntSequence { private: int value; public: IntSequence(int initialValue) :value(initialValue) {} int operator()() { return ++value; } }; int main() { vector
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 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开始插入
演示案例
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
(sum) / static_cast (num); } }; int main() { vector 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临时对象进行返回