类FuncObjType中重载了“()”操作符,因此对于一个该类的对象FuncObjType val,可以这样调用该操作符:val()。调用结果即输出以上代码中的内容。该调用语句在形式上跟以下函数的调用完全一样:
既然用函数对象与调用普通函数有相同的效果,为什么还有搞这么麻烦定义一个类来使用函数对象?主要在于函数对象有以下的优势:
1. 函数对象可以有自己的状态。我们可以在类中定义状态变量,这样一个函数对象在多次的调用中可以共享这个状态。但是函数调用没这种优势,除非它使用全局变量来保存状态。呃,面向对象编程。。。全局变量。。。。
2. 函数对象有自己特有的类型,而普通函数无类型可言。这种特性对于使用C++标准库来说是至关重要的。这样我们在使用STL中的函数时,可以传递相应的类型作为参数来实例化相应的模板,从而实现我们自己定义的规则。比如自定义容器的排序规则。
举几个使用函数对象的典型的例子:
第一个即自定义容器set对字符串string的排序规则。首先来定义相应的类并重载 “()” 来规定排序(这里我们按从大到小排序),
然后我们可以用这个类作为参数来初始化set容器:
这样容器内容输出为:“B“,”A",而不是默认情况下的"A","B"。
再举一个实用到内部状态的函数对象的例子。设想我们通过一个函数对象来产生一串连续的数字,可以任意规定起始数字,每调用一次,将返回下一个数字。相应的类可以如下设计:
为了测试,我们用到一个标准库函数:generate_n。这个函数自动调用我们传进去的函数对象n次,并将结果保存到指定的起始迭代器处。如下:
这里我们用dest来存放数字,初始化了一个函数对象SuccessiveNumGen(3),即起始数字为3。generate_n函数自动调用10次,结果存放在dest容器中。这里还用到了back_inserter函数,这是个函数适配器,它返回一个函数对象,能保证每次将结果放到容器dest的尾部。 函数适配器在后面会介绍,这里不再多说。我们只当它提供了我们存放数字的位置即可(切不可直接使用dest.end()!)。这样,最终容器中的内容为 3,4,5,6,……,12。通过使用不同的起始数字来初始化不同的函数对象,可以生成不同的数字序列。
此外,函数对象还有一个非常重要的用途,即作为谓词函数(Predicate)。谓词函数通常用来对传进来的参数进行判断,并返回布尔值。标准库中有大量的函数都定义了多个重载版本,其中包含由用户提供谓词函数的,比如:find_if,remove_if,等等。现在假设我们有一串数字,要从中找出第一个不小于10的数字。可以定义如下相应的类:
从而这样调用 find_if函数:find_if(dest.begin(),dest.end(),NoLess(10))即可,该函数返回第一个令函数对象返回true的值的位置(在这个例子中即10对应的迭代器位置)。
有一点需要指出的是,在调用用到函数对象的标准库算法时,除非显式地指定模板类型为“传引用”,否则默认情况下函数对象是”按值传递“的!因此,如果传递的是一个具有内部状态的函数对象,则被改变状态的是函数内部被复制的临时对象,函数结束后随之消失。真正传进来的函数对象状态并为改变。我们写个简单的测试程序来验证下,这个例子中我们来查找数字序列中的第3个数字:
测试如下:
输出结果为,确实能找到第三个数字(5),但查看nth的状态时,返回的m_count依然为0。说明nth确实未被修改。
一般情况下,至于是传值还是传引用,不会对我们造成太多影响。但还是有必要清楚这一点,困为有时候针对特定的实现会出现一些莫名其妙的问题,以下面的例子来说明:
我们通过remove_if函数来删除一个数字序列中的第3个数字,如下:
按正常思路来讲,删除第3个元素后输出应该是:1,2,4,5,6,7,8,9,10,10(至于最后为什么会出现两次10,这跟标准算法库设计思想有关:算法库绝不改变容器的大小!所谓的remove只是概念上的remove,被删的元素被丢到了后面,可以通过返回值来确定位置。标准算法库以后会进行说明,不是这部分的重点)。但实际情况却令人吃惊:1,2,4,5,7,8,9,10,9,10。
造成这种现象的原因与标准库的实现有关,在《The C++ Standard Library, A tutorial and reference》这本书上,作者给出一种造成这种情况的可能的实现:
在这个实现中,我们清晰地看出,函数对象op两次作为参数传递到find_if和remove_copy_if函数中,正是由于标准库默认的"按值传递”,导致两次传递进来的op都是最初始的状态,即m_count为0!这样一来,传递到remove_if函数中的op将从+next位置(next是第一次找到的将被删除的第三个位置)算起再次删除第三个元素,于是第六个元素也就被删除了。
此外,C++参考手册网站C++参考手册上提供的是这样一种可能的实现:
显然,这种实现则不会造成删除两次的情况,经过我的测试,结果确实是我们想要的:1,2,4,5,6,7,8,9,10,10。
可见,不是所有的返回布尔值的函数对象都适合作为谓词函数,比如这个例子,不同标准库具体的实现会造成不同的结果。因此,作为一个好的编程习惯,用作谓词函数的函数对象,其判断结果最好不要依赖内部变动的状态。
除了自定义的函数对象,标准库还为我们提供了一系列现成的函数对象, 比如常见的数学、逻辑运算等。例如:negate<type>(),plus<type>(),minus<type>(),multiplies<type>(),divides<type>(),modulus<type>(),equal_to<type>,greater<type>(),less<type>(),logical_not<type>(),logical_and<type>(),等等。
关于函数对象的最后一个很重要的概念是“函数适配器”。函数适配器,本质上讲也是一个函数对象。这个函数对象通过将一个或多个函数对象或者特定的数据按一定规则组合起来,以完成某些特定的功能。标准库为我们提供了几种函数适配器,例如:通过bind1st和bind2nd两个包装函数即可返回相应的适配器,这两个函数各有两个形参,分别为二元函数对象和一个数值,适配器自动把数值赋给函数对象的第1个(bind1st)或第2个(bind2nd)参数,并返回一个一元的函数对象。例如以下语句:
注意:不要误把bind1st和bind2nd当成是函数适配器,它们仅仅是两个普通的包装函数模板,它们返回的才是真正的函数适配器。对应的函数对象类型分别为binder1st和binder2nd。这两个函数模板实现如下所示: 先不管具体细节,后面马上会说到。我们只关注函数的实现,很容易看出返回的分别是类binder1st<Operation>和binder2nd<Operation>的对象,即我们所说的函数适配器。
除了这两个函数适配器,标准库中还定义了两个"取反器",分别可以通过调用辅助函数not1和not2来获得,即unary_negate<type>和binary_negate<type>。此外,包装函数模板mem_func_ref返回的适配器用于自动调用对象的成员函数,具体细节及其他适配器可以到C++参考网站查看。
函数适配器是C++中一个强有力的工具,它允许我们针对多个函数对象按照需要进行任意组合,来产生意义丰富的表达式。这种方法被称为"function composition"。比如对于函数f(x),g(x),h(x,y),对他们进行组合,可以生成f(g(x)),g(f(x)),h(f(x),g(x))等复杂的函数。这里f,g分别对应了一元函数对象,h对应了二元函数对象,组合函数可以认为是函数适配器。
不幸的是,标准库并没有给我们提供太多的适配器。因此,想要很好地利用它,还需要我们自己来定义。我们已经知道,自定义函数对象是件很容易的事情,但是想要让函数对象能在适配器中进行组合,则需要额外的一些工作。为了方便我们创建能在适配器中运用的函数对象,标准库定义了两个有用的结构,分别为: 这两个结构定义了几个类型成员,来表示函数对象参数和返回值的类型。其中unary_function代表一元函数对象,binary_function代表二元函数对象。通过让我们自定义的函数对象继承相应的结构,即可在函数适配器中使用。标准库的函数对象正是这样设计的,我们拿适配器binder1st作为例子来说明下,类binder1st如下: 适配器通过接受一个二元的函数对象(Operation)和一个该二元函数对象第一个参数类型的值(arg)调用构造函数。在其重载的"()"操作符函数中,只接受一个参数,对应其基类unary_function的argument_type,返回类型为unary_function的return_type。类定义中频繁用到了typename Operation::first_argument、typename Operation::second_argument_type及typename Operation::result_type,充分说明了对Operation的要求,只要Operation继承了binary_function即可。我们以一个计算pow(x,y)的函数对象为例子演示一下: 比如我们需要一个计算任意数字立方的函数对象,即可通过bind2nd(Pow<float,int>(),3)来得到。
最后,我们通过自定义一个函数适配器来结束本文。要实现的功能为: h(f(x),g(x))。首先这是个一元的函数对象,只接受一个参数x,它组合了三个函数对象H,F,G。可以这样来实现: