16.4 函数对象
因此,我们希望向f?ind_if()传递断言,同时希望断言能够将元素与以参数形式传递的值进行比较。特别地,我们希望能编写如下形式的代码:
显然,Larger_than必须满足如下条件:
能作为断言被调用,例如,pred(*f?irst);
能够存储一个数值,例如31或x,以备调用时使用。
为了满足这些条件,我们需要“函数对象”,即一种能够实现函数行为的对象。我们需要对象的原因是对象能够存储数据,例如待比较的值。举例来说:
有趣的是,此定义就能使前面的例子正常工作了。现在,我们需要弄明白它为何能奏效。当我们调用Larger_than(31)时,(显然)我们创建了一个Larger_than类对象,其数据成员v保存了值31。例如:
在这里,我们将对象Larger_than(31)作为参数pred的实参传递给f?ind_if()。对v的每个元素,f?ind_if()会调用:
这对我们的函数对象调用名为operator()的调用运算符,传递给它的参数是*f?irst。结果将是元素值*f?irst和31的比较结果。
我们在这里看到的是:一个函数调用可被视为一个运算符——()运算符。“()运算符”也被称为函数调用运算符(function call operator)和应用运算符(application operator)。因此,pred(*f?irst)中的()由Larger_than::operator ()赋予含义,就像v[i]中的下标操作由vector::operator[]赋予含义一样。
16.4.1 函数对象的抽象视图
我们已经学习了一种机制,允许一个“函数”“随身携带”它所要的数据。显然,函数对象为我们提供了一种非常通用、强大且便利的机制。下面的例子展示了函数对象的更一般性的概念:
类F的对象用其成员s存储数据。如果需要,一个函数对象可以拥有很多数据成员。某个对象保存数据的另一种表达方式是称其“具有状态”。当我们创建一个F时,可以初始化其状态。当需要时,我们可以读取其状态。对于F,我们提供了一个操作state()来读取状态,还提供了另一个操作reset()来设置状态。不过,我们设计一个函数对象时可以根据需要提供任何访问状态的方法。我们当然也可以直接或间接地通过普通函数调用语法来调用函数对象。在上面代码中,我们定义F在被调用时只接收一个参数,但可以根据需要定义接受多个参数的函数对象。
函数对象的使用是STL中最主要的参数化方法。我们通过函数对象指定需要查找的数据(见16.3节),定义排序标准(见16.4.2节),在数值算法中指定算术运算(见16.5节),定义值相等的含义(见16.8节)以及其他很多事情。函数对象的使用是灵活性和通用性的主要源泉。
函数对象通常是十分高效的。特别地,向一个模板函数以传值的方式传递一个小的函数对象通常能够带来优化的性能。原因很简单,但对于熟悉将函数作为参数传递的人来说可能是奇怪的:传递函数对象所产生的代码通常远比传递函数所产生的代码更小、更快。但这一结论仅当函数对象较小(如只占0、1或2个字)或者采用的是引用方式传递,并且函数调用运算符比较简单(如简单的比较操作<)且定义为内联方式(例如定义在类内)时才是正确的。本章中——以及本书中——的大多数例子都满足这些条件。小且简单的函数对象能够带来高性能的基本原因在于它们保留了足够的类型信息供编译器产生优化代码。甚至老旧的没有复杂优化器的编译器都能够为Larger_than中的比较操作生成一条简单的“大于”机器指令而不是生成一个函数调用。一次函数调用所需花费的时间通常是执行一条简单比较操作所花费时间的10到50倍。另外,函数调用所产生的代码通常是简单比较操作所产生代码的数倍之大。
16.4.2 类成员上的断言
我们已经看到,标准算法能够正确处理由基本类型(如int和double)元素组成的序列。但是,在一些应用领域,类对象的容器更为常见。下面这个例子是很多领域中应用的关键操作——根据多个标准对记录进行排序:
我们有时希望根据名字对vr进行排序,有时又希望根据地址进行排序。除非我们能同时优雅、高效地实现这两种排序标准,否则我们的技术的实用价值就会受到局限。幸运的是,同时实现两种排序标准并不难。我们可以编写如下代码:
Cmp_by_name函数对象通过比较name成员来比较两个Record。Cmp_by_addr函数对象则通过比较addr成员来比较两个Record。为了允许用户指定比较标准,标准库算法sort接受可选的第三参数用以指定比较标准。Cmp_by_name()为sort()构造了一个Cmp_by_name对象,用来比较Record。这看起来不错——意思是我们不介意维护这样的代码。现在,我们所要做的就是定义Cmp_by_name和Cmp_by_addr:
Cmp_by_name类的实现十分简单。函数调用运算符operator ()()简单地用标准string的<运算符对name字符串进行比较。但Cmp_by_addr中的比较操作很丑陋。这是因为我们采用了一种丑陋的方式表示地址:24个字符的数组(非0结尾)。之所以采用这种方式,一部分原因是为了展示函数对象是如何用于掩盖丑陋且容易产生错误的代码的,另一部分原因是这种特别的表示方式曾被作为一个挑战呈现给我:“一个STL不能处理的丑陋而又重要的现实问题。”实际上,STL能够处理。比较函数使用了标准C(和C++)库函数strncmp(),该函数能够比较固定长度的字符数组,当第二个“字符串”在字典序中排在第一个“字符串”之后时它返回一个负数。假如你需要进行这种晦涩的比较操作,可以查阅此参数(如附录C.11.3)。
16.4.3 lambda表达式
我们通常在程序中某处定义一个函数对象(或一个函数),然后在其他地方使用它,这有些令人厌烦。如果想要执行的操作很容易说明、很容易理解且之后再不会用到的话,还必须这么做就更令人生厌了。这种情况下,我们可以使用lambda表达式(见20.3.3节)。可能思考lambda表达式的最好方式是将它看作定义一个函数对象(具有()运算符的类)然后立即创建其对象的一种简写语法。例如,我们可以像下面这样编写代码:
对于此例,我们怀疑一个命名的函数对象是否会增加代码维护的负担,而且也许Cmp_by_name和Cmp_by_addr还有其他用途。
但是,考虑16.4节的f?ind_if()例子。在那个例子中,我们需要将操作作为参数传递,且此操作需要携带数据:
还有一种等价的替代方法:
lambda版本可以与局部变量x进行比较,这令它更具吸引力。