第21章 理解函数对象
函数对象(也叫functor)听起来陌生或难以理解,但它们是C++实体,即使您没有用过,也很可能见过,只是您没有意识到而已。
在本章中,您将学习:
• 函数对象的概念;
• 将函数对象用作谓词;
• 如何使用函数对象实现一元和二元谓词。
21.1 函数对象与谓词的概念
从概念上说,函数对象是用作函数的对象;但从实现上说,函数对象是实现了operator()的类的对象。虽然函数和函数指针也可归为函数对象,但实现了operator()的类的对象才能保存状态(即类的成员属性的值),才能用于标准模板库(STL)算法。
C++程序员常用于STL算法的函数对象可分为下列两种类型。
• 一元函数:接受一个参数的函数,如f(x)。如果一元函数返回一个布尔值,则该函数称为谓词。
• 二元函数:接受两个参数的函数,如 f(x, y)。如果二元函数返回一个布尔值,则该函数称为二元谓词。
返回布尔类型的函数对象通常用于需要进行判断的算法。组合两个函数对象的函数对象称为自适应函数对象。
21.2 函数对象的典型用途
可以通过长篇大论从理论上解释函数对象,也可通过小型应用程序看看函数对象是什么样的及其工作原理。下面将采取后一种实用方法,直接看看如何在C++编程中使用函数对象。
只对一个参数进行操作的函数称为一元函数。一元函数的功能可能很简单,如在屏幕上显示元素,如下所示:
函数FuncDisplayElement接受一个类型为模板化类型elementType的参数,并使用控制台输出语句 std::cout将该参数显示出来。该函数也可采用另一种表现形式,即其实现包含在类或结构的operator()中:
DisplayElement 是一个结构,如果它是类,则必须给 operator()指定访问限定符 public。结构相当于成员默认为公有的类。
这两种实现都可用于STL算法for_each,将集合中的内容显示在屏幕上,每次显示一个元素,如程序清单21.1所示。
程序清单21.1 使用一元函数将集合的内容显示在屏幕上
输出:
分析:
第8~15行包含函数对象DisplayElement,它实现了operator()。在第32~34行,将这个函数对象用于了STL算法std::for_each。for_each接受3个参数:第1个参数指定范围的起点,第2个参数指定范围的终点,第 3 个参数是要对指定范围内的每个元素调用的函数。换句话说,这些代码将对 vector verIntergers中的每个元素调用DisplayElement::operator()。注意,在这里可不使用结构DisplayElement,而使用FuncDisplayElement,其效果相同。第40~42行显示了字符list的内容。
C++11引入了lambda表达式,即匿名函数对象。
在程序清单21.1中,如果不使用结构 struct DisplayElement<T>,而使用 lambda表达式,可极大地简化代码。为此,删除定义该结构的代码,并使用如下代码替换 main()函数中使用该结构的3行代码(第32-34行):
引入lambda表达式是C++的一项重大改进,请务必阅读第22章,更深入地学习lambda表达式。在程序清单22.1中,在for_each中使用了lambda表达式来显示容器的内容,而不像程序清单21.1那样使用函数对象。
如果能够使用结构的对象来存储信息,则使用在结构中实现的函数对象的优点将显现出来。这是FuncDisplayElement 不像结构那么强大的地方,因为结构除 operator()外还可以有成员属性。下面是一个稍做修改的版本,它使用了成员属性:
在上述代码中,DisplayElementKeepCount 对前一个版本稍做了修改。operator()不再是 const 成员函数,因为它对成员Count进行递增(修改),以记录自己被调用用于显示数据的次数。该计数是通过公有成员属性Count暴露的。程序清单21.2演示了使用可保存状态的函数对象的优点。
程序清单21.2 使用函数对象存储状态
输出:
分析:
这个例子与程序清单 21.1 所示示例的最大区别在于,将 DisplayElementKeepCount 用作 for_each的返回值。算法for_each()对容器中的每个元素调用结构DisplayElementKeepCount实现的operator()时,operator()显示该元素,并递增存储在Count中的内部计数。在for_each执行完毕后,第38行使用这个对象指出显示了多少个元素。注意,在这种情况下,如果使用普通函数而不是在结构中实现的函数,将无法以如此直接的方式提供这种功能。
返回布尔值的一元函数是谓词。这种函数可供STL算法用于判断。程序清单21.3所示的谓词判断输入元素是否为初始值的整数倍。
程序清单21.3 一个一元谓词,它判断一个数字是否为另一个数字的整数倍
分析:
这里的operator()返回布尔值,可用作一元谓词。该结构有一个构造函数,它初始化除数的值。然后用保存在对象中的这个值来判断要比较的元素是否可以被它整除,如operator()的实现所示,它使用数学运算取模%来返回除法运算的余数。然后将余数与零进行比较,以判断被除数是否为除数的整数倍。
在程序清单21.4中,使用了程序清单21.3所示的谓词,来判断集合中的数是否为用户输入的除数的整数倍。
程序清单21.4 在std::find_if()中使用一元谓词IsMutilple,在vector中查找一个能被用户提供的除数整除的元素
输出:
分析:
这个例子首先声明了一个整型vector,第11~15行将一些值插入到该容器中。第21~23行的find_if使用了一元谓词。这里将函数对象IsMutilple初始化为用户提供的除数,find_if对指定范围内的每个元素调用一元谓词IsMutilple::operator()。当operator()返回true(即元素可被用户提供的除数整除)时,find_if返回一个指向该元素的迭代器。然后,将find_if()操作的结果与容器的end()进行比较,以核实是否找到了满足条件的元素,如第25行所示。接下来使用迭代器iElement显示该元素的值,如第28行所示。
要了解如何使用lambda表达式简化程序清单21.4所示的程序,请参阅程序清单22.3。
一元谓词被大量用于STL算法中。例如,std::partition算法使用一元谓词来划分范围,stable_partition算法也使用一元谓词来划分范围,但保持元素的相对顺序不变。诸如 std::find_if()等查找函数以及std::remove_if()等删除元素的函数也使用一元谓词,其中std::remove_if()删除指定范围内满足谓词条件的元素。
如果函数 f(x, y)根据输入参数返回一个值,它将很有用。这种二元函数可用于对两个操作数执行运算,如加、减、乘、除等。下面的二元函数返回输入参数的积:
同样,在上述实现中最重要的是operator(),它接受两个参数并返回它们的积。在std::transform等算法中,可使用该二元函数计算两个容器内容的乘积。程序清单21.5演示了如何在std::transform中使用该二元函数。
程序清单21.5 使用二元函数将两个范围相乘
输出:
分析:
第5~13行包含类Multiply,这与前一个代码示例相同。在这个示例中,使用算法std::transform将两个范围的内容相乘,并将结果存储在第三个范围中。在这里,这三个范围分别存储在类型为std::vector的vecMultiplicand、vecMultiplier和vecResult中。在第35~39行,使用std::transform将vecMultiplicand中的每个元素与vecMultiplier中对应的元素相乘,并将结果存储在vecResult中。乘法运算是通过调用二元函数Multiply::operator()执行的,对源范围和目标范围内的每个元素都调用了该函数。operator()的返回值保存在vecResult中。
这个示例演示了如何使用二元函数对STL容器中的元素执行算术运算。
接受两个参数并返回一个布尔值的函数是二元谓词。这种函数用于诸如std::sort()等STL函数中,程序清单21.6使用了一个二元谓词,它将两个字符串都转换为小写,再对其进行比较。这个谓词可用于对字符串vector进行不区分大小写的排序。
程序清单21.6 对字符串进行不区分大小写排序的二元谓词
分析:
在operator()中实现的二元谓词中,首先使用std::transform()将输入字符串转换为小写,如第15行和第 20 行所示;然后使用字符串的比较运算符<进行比较,并返回结果。该二元谓词可用于算法std::sort(),对包含在字符串vector中的动态数组进行排序,如程序清单21.7所示。
程序清单21.7 使用函数对象CompareStringNoCase对字符串vector进行不区分大小写的排序
输出:
分析:
输出显示了 vector 在三个阶段的内容。第一次按插入顺序显示内容。第二次是在使用默认排序谓词less<T>重新排序(如第28行所示)后进行的;输出表明,jim没有紧跟在Jack后面,这是因为使用string::operator<排序时区分大小写。为确保jim紧跟在Jack后面(虽然大小写不同),最后一次显示内容前,使用了程序清单21.6实现的排序谓词CompareStringNoCase<>,如第32行所示。
很多STL算法都使用二元谓词。例如,删除相邻重复元素的std::unique()、排序算法std::sort()、排序并保持相对顺序的std::stable_sort()以及对两个范围进行操作的std::transform(),这些STL算法都需要使用二元谓词。
21.3 总结
本章介绍了函数对象(也叫functor)。在结构或类中实现函数对象时,它将比简单函数有用得多,因为它也可用于存储与状态相关的信息。本章还介绍了谓词,它是一类特殊的函数对象。另外,还通过一些实际示例说明了谓词的用途。
21.4 问与答
问:谓词是一种特殊的函数对象,其特殊之处何在?
答:谓词总是返回布尔值。
问:调用诸如remove_if等函数时,应使用哪种函数对象?
答:应使用通过构造函数将值作为初始状态的一元谓词。
问:对于map应使用哪种函数对象?
答:应使用二元谓词。
问:没有返回值的简单函数是否可用作谓词?
答:可以。没有返回值的函数也很有用,例如,可用于显示输入的数据。
21.5 作业
作业包括测验和练习,前者帮助读者加深对所学知识的理解,后者提供了使用新学知识的机会。请尽量先完成测验和练习题,然后再对照附录D的答案。在继续学习下一章前,请务必弄懂这些答案。
1.返回布尔值的一元函数称为什么?
2.不修改数据也不返回布尔值的函数对象有什么用?请通过示例阐述您的观点。
3.函数对象这一术语的定义是什么?
1.编写一个一元函数,它可供std::for_each用来显示输入参数的两倍。
2.进一步扩展上述谓词,使其能够记录它被调用的次数。
3.编写一个用于降序排序的二元谓词。