小喵的唠叨话:
寒假之后,小喵在家里无所事事,最近用C++写代码的时候,用到了std::sort这个函数,每次用这个函数,小喵似乎都得查一下lambda表达式的写法。正好最近很闲,不如总结一下。
在Bing上搜索 C++ lambda ,第一条记录就是MSDN上的C++ lambda的介绍。本文也是基于这篇文章来写的。
那么接下来,我们分几个部分来介绍。
一、什么是Lambda表达式
MSDN上对lambda表达式的解释:
在 C++ 11 中,lambda 表达式(通常称为 “lambda”)是一种在被调用的位置或作为参数传递给函数的位置定义匿名函数对象的简便方法。 Lambda 通常用于封装传递给算法或异步方法的少量代码行。 [1]
看了这个解释,相信大家已经理解lambda表达式是什么。简而言之,lambda表达式就是一种定义函数的简单的方法。
举一个简单的例子:求一个数的阶乘。
这是一般的函数的写法:
// 这里要求n>=0,同时n的取值不能太大,会溢出 // 为了方便,这里并没有处理上面说到的问题 int factorial(int n) { int fact = 1; for (int i = 1; i <= n; ++ i) fact *= i; return fact; }</div>
Lambda表达式的写法:
autofactorial = [](int n) { int fact = 1; for (int i = 1; i <= n; ++ i) fact *= i; return fact; };</div>
乍一看,这两种定义方式十分的相似。但其实这是两种完全不同的方式,前一种是函数定义式,而后一种是一个表达式。factorial是变量名,等于号后面的是值,也就是一个lambda表达式,本质上是一个匿名的函数。最终factorial就是一个函数。
很多时候,我们只是直接书写lambda表达式,而不需要给他一个名字。比如排序的时候,sort可以接受一个自定义的比较函数,这时候直接书写lambda表达式即可。
二、Lambda表达式的作用
由于lambda本身其实也就是一种函数的定义方式。因此它的主要作用还是和一般函数一样。但是lambda表达式相对于一般函数,又有一些功能之外的作用。参考了知乎上的一些回答 [2] ,小喵也进行了总结。
1、可以用表达式来定义函数,这样使得函数的定义和调用在一起,语意和逻辑上更为紧凑。同时,对于只是用一次的短小的函数,直接调用匿名的lambda表达式是最好的选择,这样就不需要给每个函数起名字了。 /* 起名字一直是一个很令人头疼的问题 */
2、闭包(Closure)。这个小喵的写javascript的时候时常会用到。闭包本质上就是能够访问上下文环境中变量的代码块。
这里我们简单的举个例子,还是之前的求阶乘的问题,现在我们有些提高需求。
现在需要完成下面的三种阶乘的运算:
n! = n * (n – 1) * (n – 2) * …
n!! = n * (n – 2) * (n – 4) * …
n!!! = n * (n – 3) * (n – 6) * …
要求编写3个函数,分别完成上述3种计算。
使用一般的方式写很容易实现,我们这里直接使用lambda表达式来实现:
#include <iostream> #include <functional> std::function<int(int)> getFactorialFunc(int n) { return [n](int x) { int fact = 1; for (; x >= 1; x -= n) fact *= x; return fact; }; } int main() { // 构造要求的三个函数 autofactorial1 = getFactorialFunc(1); autofactorial2 = getFactorialFunc(2); autofactorial3 = getFactorialFunc(3); // 调用 std::cout << factorial1(10) << std::endl; std::cout << factorial2(10) << std::endl; std::cout << factorial3(10) << std::endl; }</div>
编译的时候要注意,lambda表达式是C++11开始支持的,所以需要指定一下C++的版本。
g++ factorial_lambda.cpp -o factorial_lambda.out --std=c++11</div>
运行之后的结果为:
./factorial_lambda.out
3628800
3840
280
这里作为返回值的lambda表达式,可以访问先前传入的参数,这也就是闭包。具体的语法,我们后面会讲到。
3、柯里化(Currying)。这部分小喵也是第一次接触,维基百科有如下解释:
在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。 [3]
下面给出一个例子(也是实现之前的阶乘):
#include <iostream> #include <functional> // 两个参数的阶乘 int factorial(int n, int step) { int r = 1; for (; n >= 1; n -= step) { r *= n; } return r; } // curring化的阶乘 std::function<int(int)> currying_factorial(int step) { return [step](int n) { return factorial(n, step); }; } int main() { // 调用普通函数 std::cout << factorial(10, 1) << std::endl; std::cout << factorial(10, 2) << std::endl; std::cout << factorial(10, 3) << std::endl; // 调用currying函数 std::cout << currying_factorial(1)(10) << std::endl; std::cout << currying_factorial(2)(10) << std::endl; std::cout << currying_factorial(3)(10) << std::endl; return 0; }</div>
4、lambda表达式整体可以被当做函数的参数或者返回值。
闭包和currying的例子就是将整个lambda表达式作为返回值。现在再举一个作为参数的例子:
#include <iostream> #include <functional> int operate(int x, int y, const std::function<int(int, int)> &op) { return op(x, y); } int main() { autoadd = [](int x, int y) { return x + y;}; automul = [](int x, int y) { return x - y;}; std::cout << operate(10, 5, add) << std::endl; std::cout << operate(10, 5, mul) << std::endl; return 0; }</div>
运行的结果:
其实函数也可以当参数传入的(函数指针),但是lambda表达式要更为直观和灵活一些。谁能一眼看出int (*func(int))(int)究竟是什么意思呢(这是一个函数的定义,输入的参数是int,返回值是一个函数指针,函数指针对应的函数的输入和输出类型都是int)。
三、Lambda表达式的语法
看到前面的lambda表达式的各种有趣的功能,现在是不是非常迫切的想尝试一把?
ISO C++ 标准展示了作为第三个参数传递给 std::sort() 函数的简单 lambda:
#include <algorithm> #include <cmath> void abssort(float* x, unsigned n) { std::sort(x, x + n, // Lambda expression begins [](float a, float b) { return (std::abs(a) < std::abs(b)); } // end of lambda expression ); }</div>
lambda表达式的组成部分见下图:
Capture 子句(在 C++ 规范中也称为 lambda 引导。)
参数列表(可选)。 (也称为 lambda 声明符)
可变规范(可选)。
异常规范(可选)。
尾随返回类型(可选)。
“lambda 体”
接下来我们需要学习这6个部分。
1、Capture 子句
我们知道,一般情况下,函数只能访问自己的参数和外部的全局变量。而lambda表达式却可以访问上下文的变量(参见闭包的例子)。那么如何指定要访问的变量,以及访问的方式(值或者引用)呢?这就是Capture 子句要解决的问题。
Lambda 可在其主体中引入新的变量(用 C++14),它还可以访问(或 “捕获” )周边范围内的变量。 Lambda 以 Capture 子句(标准语法中的 lambda 引导 )开头,它指定要捕获的变量以及是通过值还是引用进行捕获。 有与号 ( & ) 前缀的变量通过引用访问,没有该前缀的变量通过值访问。
空 capture 子句 [ ] 指示 lambda 表达式的主体不访问封闭范围中的变量。
可以使用默认捕获模式(标准语法中的 capture-default )来指示如何捕获 lambda 中引用的任何外部变量:[&] 表示通过引用捕获引用的所有变量,而 [=] 表示通过值捕获它们。 可以使用默认捕获模式,然后为特定变量显式指定相反的模式。 例如,如果 lambda 体通过引用访问外部变量 total 并通过值访问外部变量 factor ,则以下 capture 子句等效:
[&total, factor]
[factor, &total]
[&, factor]
[factor, &]