第22章 C++lambda表达式

    lambda是一种定义匿名函数对象的简洁方式,这是C++11新增的。

    在本章中,您将学习:

    • 如何编写lambda表达式;

    • 如何将lambda表达式用作谓词;

    • 如何编写可存储和可操作状态的lambda表达式。

    22.1 lambda表达式是什么

    可将lambda表达式视为包含公有operator()的匿名结构(或类),从这种意义上说,lambda表达式属于第21章介绍的函数对象。深入分析如何编写lambda表达式前,先来看看程序清单21.1中的函数对象:

    第22章 C++lambda表达式 - 图1

    这个函数对象将一个元素显示到屏幕上,通常用于std::for_each等算法中:

    第22章 C++lambda表达式 - 图2

    如果使用lambda表达式,可将上述代码(包括函数对象的定义)简化为下述3行:

    第22章 C++lambda表达式 - 图3

    编译器见到下述lambda表达式时:

    第22章 C++lambda表达式 - 图4

    自动将其展开为类似于结构DisplayElement<int>的表示:

    第22章 C++lambda表达式 - 图5

    22.2 如何定义lambda表达式

    lambda表达式的定义必须以方括号([])打头。这些括号告诉编译器,接下来是一个lambda表达式。方括号的后面是一个参数列表,该参数列表与不使用lambda表达式时提供给operator()的参数列表相同。

    22.3 一元函数对应的lambda表达式

    与一元operator(Type)对应的lambda表达式接受一个参数,其定义如下:

    第22章 C++lambda表达式 - 图6

    请注意,如果您愿意,也可按引用传递参数:

    第22章 C++lambda表达式 - 图7

    程序清单22.1演示了如何在算法for_each中使用lambda表达式来显示标准模板库(STL)容器的内容。

    程序清单22.1 在算法for_each()中使用lambda表达式而不是函数对象来显示容器中的元素

    第22章 C++lambda表达式 - 图8

    输出:

    第22章 C++lambda表达式 - 图9

    分析:

    这里使用了两个lambda表达式,如第23和31行所示。这两个lambda表达式很像,只是输入参数不同,因为根据两个容器包含的元素类型对它们进行了定制。第一个lambda表达式接受一个int参数,并使用它来显示整型vector中的元素,每次一个;第二个lambda表达式接受一个char参数,并使用它来显示std::list中的char元素。

    第22章 C++lambda表达式 - 图10程序清单22.1的输出与程序清单21.1相同,这并非巧合。事实上,这个程序是程序清单21.1 所示程序的 lambda 表达式版,而程序清单 21.1 使用的是函数对象DisplayElement<T>。

    如果将这两个程序清单进行比较,您将发现,通过使用lambda表达式,可让C++代码更简单、更简洁。

    22.4 一元谓词对应的lambda表达式

    谓词可帮助您做出决策。一元谓词是返回bool类型(true或false)的一元表达式。lambda表达式也可返回值,例如,下面的lambda表达式在Num为偶数时返回true:

    第22章 C++lambda表达式 - 图11

    在这里,返回值的性质让编译器知道该lambda表达式的返回类型为bool。

    在算法中,可将lambda表达式用作一元谓词。例如,可在std::find_if()中使用上述lambda表达式找出集合中的偶数,如程序清单22.2所示。

    程序清单22.2 在算法std::find_if()中,将lambda表达式用作一元谓词,以查找集合中的偶数

    第22章 C++lambda表达式 - 图12

    输出:

    第22章 C++lambda表达式 - 图13

    分析:

    用作一元谓词的lambda表达式如第15行所示。算法find_if()对指定范围内的每个元素调用该一元

    谓词;如果该谓词返回true,find_if()将返回一个指向相应元素的迭代器,指出找到了一个满足条件的元素。这里的谓词是一个lambda表达式,当find_if()使用一个偶数调用它(即对2求模的结果为零)时,它将返回true。

    第22章 C++lambda表达式 - 图14程序清单22.2不但演示了如何将lambda表达式用作一元谓词,还在该lambda表达式中使用了const。

    务必使用const来限定输入参数,在输入参数为引用时尤其如此。

    22.5 通过捕获列表接受状态变量的lambda表达式

    在程序清单22.2中,您创建了一个一元谓词,它在整数能被2整除(即为偶数)时返回true。如果要让它更通用,在数字能被用户指定的除数整除时返回true,该如何办呢?为此,需要让lambda表达式接受该“状态”———除数:

    第22章 C++lambda表达式 - 图15

    一系列以状态变量的方式传递的参数([…])也被称为 lambda表达式的捕获列表(capture list)。

    第22章 C++lambda表达式 - 图16上述lambda表达式只有一行代码,但与程序清单21.3所示的16行代码等价,该程序清单以结构 IsMultiple< >的方式定义了一个一元谓词。

    这表明,C++11新增的lambda表达式大幅提高了编程效率。

    程序清单22.3演示了如何使用lambda表达式,根据状态变量在集合中查找可被用户提供的除数整除的元素。

    程序清单22.3 使用存储状态的lambda表达式来判断一个数字能否被另一个数字整除

    第22章 C++lambda表达式 - 图17

    输出:

    第22章 C++lambda表达式 - 图18

    分析:

    包含状态并用作为此的lambda表达式如第24行所示。除数是一个状态变量,相当于程序清单21.3中的IsMultiple::Divisor,因此状态变量类似于C++11之前的函数对象类中的成员。您可以将状态传递给lambda表达式,并根据状态的性质相应地使用它。

    第22章 C++lambda表达式 - 图19程序清单22.3与程序清单21.4等价,但使用的是lambda表达式,而不是函数对象。这里通过使用C++11新增的lambda表达式,减少了16行代码。

    22.6 lambda表达式的通用语法

    lambda 表达式总是以方括号打头,并可接受多个状态变量,为此可在捕获列表([…])中指定这些状态变量,并用逗号分隔:

    第22章 C++lambda表达式 - 图20

    如果要在lambda表达式中修改这些状态变量,可添加关键字multable:

    第22章 C++lambda表达式 - 图21

    这样,便可在lambda表达式中修改捕获列表([])中指定的变量,但离开lambda表达式后,这些修改将无效。要确保在lambda表达式内部对状态变量的修改在其外部也有效,应按引用传递它们:

    第22章 C++lambda表达式 - 图22

    lambda表达式还可接受多个输入参数,为此可用逗号分隔它们:

    第22章 C++lambda表达式 - 图23

    如果要向编译器明确地指定返回类型,可使用->,如下所示:

    第22章 C++lambda表达式 - 图24

    最后,复合语句({})可包含多条用分号分隔的语句,如下所示:

    第22章 C++lambda表达式 - 图25

    第22章 C++lambda表达式 - 图26如果lambda表达式包含多行代码,您必须显式地指定返回类型。

    本章后面的程序清单22.5演示了一个跨越多行并指定了返回类型的lambda表达式。

    总之,lambda表达式更简洁,从功能上说,完全可替代下面这样的函数对象:

    第22章 C++lambda表达式 - 图27

    22.7 二元函数对应的lambda表达式

    二元函数接受两个参数,还可返回一个值。与之等价的lambda表达式如下:

    第22章 C++lambda表达式 - 图28

    程序清单22.4演示了一个lambda表达式,并在std::transform中使用它将两个等长的vector中对应的元素相乘,再将结果存储到第三个vector中。

    程序清单22.4 将lambda表达式用作二元函数,以便将两个容器中的元素相乘,并将结果存储到第三个容器中

    第22章 C++lambda表达式 - 图29

    输出:

    第22章 C++lambda表达式 - 图30

    分析:

    lambda表达式如第29行所示,它被用作std::transform的一个参数。算法std::transform接受两个范围作为输入,执行二元函数指定的变换算法,并将结果存储在目标容器中。这里的二元函数是一个lambda 表达式,它接受两个整数作为输入,并返回相乘得到的结果。返回值被 std::transform 存储到vecResult中。输出指出了两个容器的内容以及将对应的元素相乘得到的结果。

    第22章 C++lambda表达式 - 图31程序清单22.4演示了与程序清单21.5中的函数对象类Multiply<>等价的lambda表达式。

    22.8 二元谓词对应的lambda表达式

    返回true或false,可帮助决策的二元函数被称为二元谓词。这种谓词可用于std::sort()等排序算法中,这些算法对容器中的两个值调用二元谓词,以确定将哪个放在前面。与二元谓词等价的lambda表达式的通用语法如下:

    第22章 C++lambda表达式 - 图32

    程序清单22.5演示了如何将lambda表达式用于排序。

    程序清单22.5 在std::sort()中,将lambda表达式用作二元谓词,以便进行区分大小写的排序

    第22章 C++lambda表达式 - 图33

    输出:

    第22章 C++lambda表达式 - 图34

    分析:

    这里演示的lambda表达式是第34~52行的std::sort()的第三个参数,该lambda表达式很长,跨越了第35~51行。它表明,lambda表达式可跨越多行,但必须显式地指定返回类型,如第35行所示。输出表明,插入元素后,jim位于Jack前面;第30行以默认方式排序(即未提供lambda表达式或谓词)后,jim位于 Sam后面,因为这将根据string::operator<以区分大小写的方式进行排序。最后,使用不区分大小写的lambda表达式排序(如第34~52行所示)后,jim紧跟在Jack后面,这符合用户的预期。另外,这个lambda表达式跨越多行。

    第22章 C++lambda表达式 - 图35程序清单22.5中的超大型lambda表达式与程序清单21.6所示的CompareStringNoCase类等效,程序清单21.7使用了CompareStringNoCase类。

    显然,在这种情况下,使用lambda表达式并非最佳选择,因为如果使用函数对象,将能在多条std::sort()语句中重用它,还可将其用于其他需要二元谓词的算法。

    因此,仅当lambda表达式简短、高效时,才应使用它。

    第22章 C++lambda表达式 - 图36

    22.9 总结

    本章介绍了C++11新增的一项非常重要的功能:lambda表达式。lambda是匿名的函数对象,可接受参数、存储状态、返回值以及跨越多行。您学习了如何在find()、sort()、transform()等STL算法中使用lambda表达式,而不是函数对象。lambda表达式可提高C++编程速度和效率,应尽可能使用它们。

    22.10 问与答

    问:是否在任何情况下都应使用lambda表达式,而不是函数对象?

    答:使用跨越多行的lambda表达式(如程序清单22.5所示)可能无助于提高编程效率,此时应使用易于重用的函数对象。

    问:lambda表达式的状态参数是如何传递的?按值传递还是按引用传递?

    答:编写下面这样包含捕获列表的lambda表达式时:

    第22章 C++lambda表达式 - 图37

    将复制状态参数Var1和Var2,而不是按引用传递它们。如果要按引用传递它们,应使用下面的语法:

    第22章 C++lambda表达式 - 图38

    在这种情况下,对状态变量所做的修改在lambda表达式外部仍将有效,因此请务必小心。

    问:在lambda表达式中,可使用函数中的局部变量吗?

    答:可使用捕获列表来传递局部变量:

    第22章 C++lambda表达式 - 图39

    要传递所有的局部变量,可使用如下语法:

    第22章 C++lambda表达式 - 图40

    22.11 作业

    作业包括测验和练习,前者帮助读者加深对所学知识的理解,后者提供了使用新学知识的机会。请尽量先完成测验和练习题,然后再对照附录D的答案。在继续学习下一章前,请务必弄懂这些答案。

    22.11.1 测验

    1.编译器如何确定lambda表达式的起始位置?

    2.如何将状态变量传递给lambda表达式?

    3.如何指定lambda表达式的返回类型?

    22.11.2 练习

    1.编写一个可用作二元谓词的lambda表达式,帮助将元素按降序排列。

    2.编写一个这样的lambda表达式,即用于for_each时,给vector等容器中的元素加上用户指定的值。