函数对象与lambda表达式

DinS          Written on 2018/4/21

本专题让我们探讨探讨一个特殊的内容:函数对象

一、函数与函数对象

当一个类重载了()运算符后,其对象的行为与调用一个函数看上去一样,就称为函数对象。比如如下代码:

运行结果都是3。

c++11标准引入了lambda表达式,其作用是构造一个临时的未命名函数对象。
因此如下代码:

与上面的AddClass是一模一样的。

事实上标准库定义了一些常用的函数对象,在头文件functional里,通常跟泛型算法搭配使用。
比如我们想要将double数组降序排列(默认是升序,使用<比较),则可以这样写

greater<double>()构造了一个函数对象,使用double的>进行比较。

二、函数对象的用武之地

函数对象第一个典型用法是与泛型算法配合,完成自定义的操作。
比如针对自定义的类型,使用泛型算法时或者重载运算符或者构造函数对象。

大图点击这里

这并没有什么稀奇的,关键是从这里引出我们对函数对象思考.
泛型算法通过函数对象提供了对算法本身的扩展,即将对数据的操作与算法框架分离。看到这里有没有联想到什么?如果你想到了OOP,那么恭喜你。
实际上函数对象与OOP有某种微妙的联系,让我们看下面这个例子。

一个简单的类体系,外加辅助函数和主函数。运行结果

符合预期(假设读者熟悉OOP的基本概念,如果不熟悉可参考《我们为什么需要面向对象编程?》等文章。)

那么函数对象与OOP有什么联系呢?
我们知道基类规定了接口,包括函数返回值、参数和名称
对于函数对象而言,头文件functional中提供了function模板,用于确定可调用对象的调用形式。
只要调用形式一致,可以传入不同的函数对象作为参数。
这么说有点抽象,看个具体的例子

大图点击这里

我们将拥有调用形式int(int)的函数对象作为参数传入函数Func_AnimalEat,这就意味着凡是接收一个int并返回一个int的函数对象都可以当作参数传入。
而在Func_AnimalEat内部我们调用函数对象操作数据本身。
在main中我们使用lambda构造临时函数对象作为参数传入。
以上代码运行结果

跟OOP的效果是一致的,而且思路上跟OOP也有相似性,你可以把lambda构造出来的函数对象想象成派生类。
不过如果你认为OOP与函数对象有某种内在的联系,那么就错了。
实际上二者是完全两种不同的思考问题的方式,解决的也是不同的问题


OOP是针对内容的,是纵向的,是思考软件结构设计的
函数对象是针对形式的,是横向的,是分离算法框架与数据操作的

希望这张图能够帮助读者理解上面两句话。
OOP是以内容为线索的,Animal-Dog-Cat,没有这些内容就没有类体系,而类体系一定是向下延伸的,即纵向发展。
函数对象不管是Animal还是Dog还是Cat,只要调用形式是int(int)都OK,所以跟内容没关系,形式统一即可,这样画在图上一定是横向发展。

所以函数对象真正的用武之地是将算法与操作分离。
泛型算法大量使用函数对象就是这个用意。
另外由于函数对象在许多情况下使用lambda构造,而lambda本身只适用于小规模的操作,写太长不方便阅读,所以如果程序中需要这种分离数据操作的内容,并且形式统一,操作短小,就可以考虑使用函数对象。
当然用得最多的还是跟泛型算法配合。