./factorial_lambda.out
3628800
这里作为返回值的lambda表达式,可以访问先前传入的参数,这也就是闭包。具体的语法,我们后面会讲到。
柯里化(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;
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;
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;
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() {
auto add = [](int x, int y) { return x + y;};
auto mul = [](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;
运行的结果:
其实函数也可以当参数传入的(函数指针),但是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,
[](float a, float b) {
return (std::abs(a) < std::abs(b));
}
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, &]
[=, &total]
[&total, =]
我们之前的闭包中使用的就是通过值访问。
使用 capture-default
时,只有 lambda
中提及的变量才会被捕获。
如果 capture 子句包含 capture-default
&
,则该 capture 子句的 identifier
中没有任何 capture
可采用 & identifier
形式。
同样,如果 capture 子句包含 capture-default
=
,则该 capture 子句的 capture
不能采用 = identifier
形式。
identifier 或 this
在 capture 子句中出现的次数不能超过一次。
以下代码片段给出了一些示例。
struct S { void f(int i); };
void S::f(int i) {
[&, i]{};
[=, &i]{};
[&, &i]{};
[=, this]{};
[i, i]{};
capture
后跟省略号是包扩展,如以下可变参数模板[4]示例中所示:
template<class... Args>
void f(Args... args) {
auto x = [args...] { return g(args...); };
x();
要在类方法的正文中使用 lambda
表达式,需要将 this
指针传递给 Capture
子句,以提供对封闭类的方法和数据成员的访问权限。
这里大家可能觉得有点奇怪,将this指针传给Capture子句?
其实我们常使用的成员函数也是用类似的方法实现的。我们知道,使用成员函数需要有一个类实例,但是调用类函数就不需要。这是因为成员函数的第一个参数是this,当然这个参数我们编写代码的时候不需要自己手动写出,而是默认的。使用像python这样的语言的时候就是需要显示的写出的。在使用类实例调用成员函数的时候,会默认将this指针传入。成员函数有这么一个参数,就可以访问类实例的各种变量和方法。而类函数是没有这个参数的,也就是没有this这个指针,因此它的调用并不需要类实例,当然也就不能访问类实例的变量。
在使用 capture 子句时,要记住以下几点(尤其是使用采取多线程的 lambda 时):
引用捕获可用于修改外部变量,而值捕获却不能实现此操作。(mutable允许修改副本,而不能修改原始项。)
引用捕获会反映外部变量的更新,而值捕获却不会反映。
引用捕获引入生存期依赖项,而值捕获却没有生存期依赖项。当 lambda 以异步方式运行时,这一点尤其重要。 如果在异步 lambda 中通过引用捕获本地变量,该本地变量将很可能在 lambda 运行时消失,从而导致运行时访问冲突。
通用捕获 (C++14) 在 C++14 中,可在 Capture
子句中引入并初始化新的变量,而无需使这些变量存在于 lambda 函数的封闭范围内。
初始化可以任何任意表达式表示;且将从该表达式生成的类型推导新变量的类型。此功能的一个好处是,在 C++14 中,可从周边范围捕获只移动的变量(例如 std::unique_ptr
)并在 lambda
中使用它们。
pNums = make_unique<vector<int>>(nums);
auto a = [ptr = move(pNums)]()
2、参数列表
除了捕获变量,lambda 还可接受输入参数。 参数列表(在标准语法中称为 lambda 声明符)是可选的,它在大多数方面类似于函数的参数列表。
auto add = [] (int first, int second)
return first + second;
在 C++14 中,如果参数类型是泛型,则可以使用 auto
关键字作为类型说明符。 这将告知编译器将函数调用运算符创建为模板。参数列表中的每个 auto
实例等效于一个不同的类型参数。
auto add = [] (auto first, auto second)
return first + second;
lambda 表达式
可以将另一个 lambda 表达式
作为其参数。
由于参数列表是可选的,因此在不将参数传递到 lambda 表达式,并且其 lambda-declarator:
不包含 exception-specification
、trailing-return-type
或 mutable
的情况下,可以省略空括号。
[]{};
3、可变规范
通常,lambda
的函数调用运算符为 const-by-value
,但对 mutable
关键字的使用可将其取消。它不会生成可变的数据成员。利用可变规范,lambda 表达式
的主体可以修改通过值捕获的变量。本文后面的一些示例将显示如何使用 mutable
。
#include <iostream>
int main()
int n = 10;
auto lambda1 = [n](int x) {
return x + n;
auto lambda2 = [n](int x) mutable {
++ n;
return x + n;
std::cout << lambda1(5) << " " << n << std::endl;
std::cout << lambda2(5) << " " << n << std::endl;
return 0;
输出的结果是:
15 10
16 10
可以看出n确实是通过值来访问,在lambda1
中,我们运行++ n
,在编译的时候会报错。使用mutable
修饰之后,就可以修改参数(副本)的值。
4、异常规范
你可以使用 throw()
异常规范来指示 lambda 表达式
不会引发任何异常。与普通函数一样,如果 lambda 表达式
声明 C4297 异常规范且 lambda 体
引发异常,Visual C++ 编译器将生成警告 throw()
,如下所示:
int main()
[]() throw() { throw 5; }();
在MSDN的异常规范[5]中,明确指出异常规范是在 C++11 中弃用的 C++ 语言功能。因此这里不建议大家使用。
5、返回类型
将自动推导 lambda 表达式
的返回类型。无需使用 auto
关键字,除非指定尾随返回类型
。trailing-return-type
类似于普通方法或函数的返回类型部分。但是,返回类型必须跟在参数列表的后面,你必须在返回类型前面包含 trailing-return-type
关键字 ->
。 如果 lambda 体
仅包含一个返回语句或其表达式不返回值,则可以省略 lambda 表达式
的返回类型部分
。如果 lambda 体
包含单个返回语句,编译器将从返回表达式的类型推导返回类型。否则,编译器会将返回类型推导为 void
。
#include <iostream>
#include <typeinfo>
int main() {
auto lambda1 = [](int i) {return i;};
auto lambda2 = [](int i) -> bool {return i;};
auto lambda3 = [](int i) -> float {return i;};
auto x1 = lambda1(10);
auto x2 = lambda2(10);
auto x3 = lambda3(10);
std::cout << x1 << " " << typeid(x1).name() << std::endl;
std::cout << x2 << " " << typeid(x2).name() << std::endl;
std::cout << x3 << " " << typeid(x3).name() << std::endl;
return 0;
typeinfo的功能是获取一个变量的类型,由于它的实现依赖于编译器,所以在不同平台下的输出可能不完全一样。小喵这边的输出是:
可以看出,三个lambda的输出是不相同的。默认情况下,会返回一个最直接的类型。
6、lambda体
lambda体其实和函数体几乎完全相同。 lambda 表达式
的 lambda 体
(标准语法中的 compound-statement
)可包含普通方法或函数的主体可包含的任何内容。普通函数和 lambda 表达式
的主体均可访问以下变量类型:
从封闭范围捕获变量,如前所述(Capture)。
本地声明变量
类数据成员(在类内部声明并且捕获 this
时)
具有静态存储持续时间的任何变量(例如,全局变量)
这里要注意我们在Capture 规范
中说到的值访问和引用访问的特点。
下面的例子都是MSDN上给出的。以下示例包含显式捕获变量 n
和引用隐式捕获变量 m
的 lambda 表达式:
#include <iostream>
using namespace std;
int main()
int m = 0;
int n = 0;
[&, n] (int a) mutable { m = ++n + a; }(4);
cout << m << endl << n << endl;
输出结果:
由于变量 n
是通过值捕获的,因此在调用 lambda 表达式
后,变量的值仍保持 0
不变。 mutable
规范允许在 lambda 中修改 n
。
尽管 lambda 表达式
只能捕获具有自动存储持续时间的变量,但你可以在 lambda 表达式
的主体中使用具有静态存储持续时间的变量。
以下示例使用 generate
函数和 lambda 表达式
为 vector
对象中的每个元素赋值。 lambda 表达式
将修改静态变量以生成下一个元素的值。
void fillVector(vector<int>& v)
static int nextValue = 1;
generate(v.begin(), v.end(), [] { return nextValue++; });
四、应用Lambda的比较函数的编写
为什么要补充这一部分呢?因为我们在写程序的时候,往往最常用到lambda的地方就是数组的sort。 首先,我们知道std::sort默认是接受2个参数的,表示需要排序的序列的开始和结尾。对于一些复杂的数据类型,我们可以给它添加一个用来比较的函数 operator <
。但更多的是通过给sort添加第三个参数来实现。而这个参数就是一个比较器。
sort默认使用 <
比较符来进行比较,排序的结果是升序。我们写的比较函数的功能就是代替 <
。记住这个特点,就不会在编写比较函数的时候理不清思路。
这里举一个小例子,给一组点坐标,按欧氏距离排序:
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector< pair<int, int> > arr;
arr.push_back(make_pair(1, 4));
arr.push_back(make_pair(2, 3));
arr.push_back(make_pair(5, 7));
arr.push_back(make_pair(6, 2));
sort(arr.begin(), arr.end(),
[](pair<int, int> left, pair<int, int> right) {
int d1 = left.first * left.first + left.second * left.second;
int d2 = right.first * right.first + right.second * right.second;
return d1 < d2;
});
for (auto &p: arr) {
cout << "(" << p.first << ", " << p.second << ")" << endl;
return 0;
输出结果:
(2, 3)
(1, 4)
(6, 2)
(5, 7)
唯一需要注意的是,我们的比较函数取代的是 <
。
至此,关于Lambda 表达式
的介绍也就结束了。说得不够详细的地方,请大家参考MSDN的官方文档,写的真心不错。小喵这里有一些地方都是直接照搬过来的。https://msdn.microsoft.com/zh-cn/library/dd293608.aspx
最近在类中使用了lambda表达式
,结果出现奇怪的问题,故记录下来。lambda表达式
可以允许捕获局部变量,但是类成员变量并不是局部变量,因此不能被lambda捕获。我们想要在lambda中使用类成员变量的话,只需要捕获this
指针就可以。之后在lambda函数体中就可以随时使用成员变量了。对于this的捕获,永远是值传递的方式,即使指定了默认捕获的方式为引用,另外[&this]
这样的捕获方式是不允许的。
转载请注明出处~
[1] https://msdn.microsoft.com/zh-cn/library/dd293608.aspx
[2] https://www.zhihu.com/question/20125256
[3] https://zh.wikipedia.org/wiki/柯里化
[4] https://msdn.microsoft.com/zh-cn/library/dn439779.aspx
[5] https://msdn.microsoft.com/zh-cn/library/wfa0edys.aspx