- 内容提要
- 前言
- 第 1 章 预备知识
- 第 2 章 开始学习 C++
- 第 3 章 处理数据
- 第 4 章 复合类型
- 第 5 章 循环和关系表达式
- 第 6 章 分支语句和逻辑运算符
- 第 7 章 函数——C++的编程模块
- 第 8 章 函数探幽
- 第 9 章 内存模型和名称空间
- 第 10 章 对象和类
- 第 11 章 使用类
- 第 12 章 类和动态内存分配
- 第 13 章 类继承
- 第 14 章 C++中的代码重用
- 第 15 章 友元、异常和其他
- 第 16 章 string 类和标准模板库
- 第 17 章 输入、输出和文件
- 第 18 章 探讨 C++新标准
- 附录 A 计数系统
- 附录 B C++保留字
- 附录 C ASCII 字符集
- 附录 D 运算符优先级
- 附录 E 其他运算符
- 附录 F 模板类 string
- 附录 G 标准模板库方法和函数
- 附录 H 精选读物和网上资源
- 附录 I 转换为 ISO 标准 C++
- 附录 J 复习题答案
16.5 函数对象
很多 STL 算法都使用函数对象——也叫函数符(functor)。函数符是可以以函数方式与( ) 结合使用的任意对象。这包括函数名、指向函数的指针和重载了( ) 运算符的类对象(即定义了函数 operator( )( ) 的类)。例如,可以像这样定义一个类:
这样,重载的( ) 运算符将使得能够像函数那样使用 Linear 对象:
其中 y1 将使用表达式 0 + 1 * 12.5 来计算,y2 将使用表达式 10.0 + 2.5 * 0.4 来计算。在表达式 y0 + slope * x 中,y0 和 slope 的值来自对象的构造函数,而 x 的值来自 operator( ) ( ) 的参数。
还记得函数 for_each 吗?它将指定的函数用于区间中的每个成员:
通常,第 3 个参数可以是常规函数,也可以是函数符。实际上,这提出了一个问题:如何声明第 3 个参数呢?不能把它声明为函数指针,因为函数指针指定了参数类型。由于容器可以包含任意类型,所以预先无法知道应使用哪种参数类型。STL 通过使用模板解决了这个问题。for_each 的原型看上去就像这样:
ShowReview( ) 的原型如下:
这样,标识符 ShowReview 的类型将为 void(*)(const Review &),这也是赋给模板参数 Function 的类型。对于不同的函数调用,Function 参数可以表示具有重载的( ) 运算符的类类型。最终,for_each( ) 代码将具有一个使用 f( ) 的表达式。在 ShowReview( ) 示例中,f 是指向函数的指针,而 f( ) 调用该函数。如果最后的 for_each( ) 参数是一个对象,则 f( ) 将是调用其重载的( ) 运算符的对象。
16.5.1 函数符概念
正如 STL 定义了容器和迭代器的概念一样,它也定义了函数符概念。
- 生成器(generator)是不用参数就可以调用的函数符。
- 一元函数(unary function)是用一个参数可以调用的函数符。
- 二元函数(binary function)是用两个参数可以调用的函数符。
例如,提供给 for_each( ) 的函数符应当是一元函数,因为它每次用于一个容器元素。
当然,这些概念都有相应的改进版:
- 返回 bool 值的一元函数是谓词(predicate);
- 返回 bool 值的二元函数是二元谓词(binary predicate)。
一些 STL 函数需要谓词参数或二元谓词参数。例如,程序清单 16.9 使用了 sort( ) 的这样一个版本,即将二元谓词作为其第 3 个参数:
list 模板有一个将谓词作为参数的 remove_if( ) 成员,该函数将谓词应用于区间中的每个元素,如果谓词返回 true,则删除这些元素。例如,下面的代码删除链表 three 中所有大于 100 的元素:
最后这个例子演示了类函数符适用的地方。假设要删除另一个链表中所有大于 200 的值。如果能将取舍值作为第二个参数传递给 tooBig( ),则可以使用不同的值调用该函数,但谓词只能有一个参数。然而,如果设计一个 TooBig 类,则可以使用类成员而不是函数参数来传递额外的信息:
这里,一个值(V)作为函数参数传递,而第二个参数(cutoff)是由类构造函数设置的。有了该定义后,就可以将不同的 TooBig 对象初始化为不同的取舍值,供调用 remove_if( ) 时使用。程序清单 16.15 演示了这种技术。
程序清单 16.15 functor.cpp
一个函数符(f100)是一个声明的对象,而另一个函数符(TooBig<int>(200))是一个匿名对象,它是由构造函数调用创建的。下面是程序清单 16.15 中程序的输出:
假设已经有了一个接受两个参数的模板函数:
则可以使用类将它转换为单个参数的函数对象:
即可以这样做:
因此,调用 tB100(x) 相当于调用 tooBig(x, 100),但两个参数的函数被转换为单参数的函数对象,其中第二个参数被用于构建函数对象。简而言之,类函数符 TooBig2 是一个函数适配器,使函数能够满足不同的接口。
在该程序清单中,可使用 C++11 的初始化列表功能来简化初始化。为此,可将如下代码:
替换为下述代码:
16.5.2 预定义的函数符
STL 定义了多个基本函数符,它们执行诸如将两个值相加、比较两个值是否相等操作。提供这些函数对象是为了支持将函数作为参数的 STL 函数。例如,考虑函数 transform( )。它有两个版本。第一个版本接受 4 个参数,前两个参数是指定容器区间的迭代器(现在您应该已熟悉了这种方法),第 3 个参数是指定将结果复制到哪里的迭代器,最后一个参数是一个函数符,它被应用于区间中的每个元素,生成结果中的新元素。例如,请看下面的代码:
上述代码计算每个元素的平方根,并将结果发送到输出流。目标迭代器可以位于原始区间中。例如,将上述示例中的 out 替换为 gr8.begin( ) 后,新值将覆盖原来的值。很明显,使用的函数符必须是接受单个参数的函数符。
第 2 种版本使用一个接受两个参数的函数,并将该函数用于两个区间中元素。它用另一个参数(即第 3 个)标识第二个区间的起始位置。例如,如果 m8 是另一个 vector<double>对象,mean(double,double)返回两个值的平均值,则下面的的代码将输出来自 gr8 和 m8 的值的平均值:
现在假设要将两个数组相加。不能将+作为参数,因为对于类型 double 来说,+是内置的运算符,而不是函数。可以定义一个将两个数相加的函数,然后使用它:
然而,这样必须为每种类型单独定义一个函数。更好的办法是定义一个模板(除非 STL 已经有一个模板了,这样就不必定义)。头文件 functional(以前为 function.h)定义了多个模板类函数对象,其中包括 plus< >( )。
可以用 plus< >类来完成常规的相加运算:
它使得将函数对象作为参数很方便:
这里,代码没有创建命名的对象,而是用 plus<double>构造函数构造了一个函数符,以完成相加运算(括号表示调用默认的构造函数,传递给 transform( ) 的是构造出来的函数对象)。
对于所有内置的算术运算符、关系运算符和逻辑运算符,STL 都提供了等价的函数符。表 16.12 列出了这些函数符的名称。它们可以用于处理 C++内置类型或任何用户定义类型(如果重载了相应的运算符)。
表 16.12 运算符和相应的函数符
运 算 符 | 相应的函数符 |
---|---|
+ | plus |
- | minus |
* | multiplies |
/ | divides |
% | modulus |
- | negate |
= = | equal_to |
! = | not_equal_to |
> | greater |
< | less |
>= | greater_equal |
<= | less_equal |
&& | logical_and |
‖ | logical_or |
! | logical_not |
警告:
老式 C++实现使用函数符名 times,而不是 multiplies。
16.5.3 自适应函数符和函数适配器
表 16.12 列出的预定义函数符都是自适应的。实际上 STL 有 5 个相关的概念:自适应生成器(adaptable generator)、自适应一元函数(adaptable unary function)、自适应二元函数(adaptable binary function)、自适应谓词(adaptable predicate)和自适应二元谓词(adaptable binary predicate)。
使函数符成为自适应的原因是,它携带了标识参数类型和返回类型的 typedef 成员。这些成员分别是 result_type、first_argument_type 和 second_argument_type,它们的作用是不言自明的。例如,plus<int>对象的返回类型被标识为 plus<int>::result_type,这是 int 的 typedef。
函数符自适应性的意义在于:函数适配器对象可以使用函数对象,并认为存在这些 typedef 成员。例如,接受一个自适应函数符参数的函数可以使用 result_type 成员来声明一个与函数的返回类型匹配的变量。
STL 提供了使用这些工具的函数适配器类。例如,假设要将矢量 gr8 的每个元素都增加 2.5 倍,则需要使用接受一个一元函数参数的 transform( ) 版本,就像前面的例子那样:
multiplies( ) 函数符可以执行乘法运行,但它是二元函数。因此需要一个函数适配器,将接受两个参数的函数符转换为接受 1 个参数的函数符。前面的 TooBig2 示例提供了一种方法,但 STL 使用 binder1st 和 binder2nd 类自动完成这一过程,它们将自适应二元函数转换为自适应一元函数。
来看 binder1st。假设有一个自适应二元函数对象 f2( ),则可以创建一个 binder1st 对象,该对象与一个将被用作 f2( ) 的第一个参数的特定值(val)相关联:
这样,使用单个参数调用 f1(x) 时,返回的值与将 val 作为第一参数、将 f1( ) 的参数作为第二参数调用 f2( ) 返回的值相同。即 f1(x) 等价于 f2(val, x),只是前者是一元函数,而不是二元函数。f2( ) 函数被适配。同样,仅当 f2( ) 是一个自适应函数时,这才能实现。
看上去有点麻烦。然而,STL 提供了函数 bind1st( ),以简化 binder1st 类的使用。可以问其提供用于构建 binder1st 对象的函数名称和值,它将返回一个这种类型的对象。例如,要将二元函数 multiplies( ) 转换为将参数乘以 2.5 的一元函数,则可以这样做:
因此,将 gr8 中的每个元素与 2.5 相乘,并显示结果的代码如下:
binder2nd 类与此类似,只是将常数赋给第二个参数,而不是第一个参数。它有一个名为 bind2nd 的助手函数,该函数的工作方式类似于 bind1st。
程序清单 16.16 将一些最近的示例合并成了一个小程序。
程序清单 16.16 funadap.cpp
程序清单 16.16 中程序的输出如下:
C++11 提供了函数指针和函数符的替代品——lambda 表达式,这将在第 18 章讨论。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论