C++ lambda

一、lambda是什么

在 C++ 11 及更高版本中,Lambda 表达式(通常称为 Lambda)用来定义匿名函数对象,用来作为参数传递给算法或者作为匿名函数进行调用,使得代码更加灵活简洁。

二、lambda语法

lambda语法组成:

[capture-list] (parameters) mutable -> return-type { statement }

lambda语法

2.1 capture-list

capture-list可以把上下文变量以值或引用的方式捕获,在statement中直接使用,若capture-list为空,则不捕获任何变量。

  • [var] : 表示值传递方式捕捉变量

  • [=] : 表示值传递方式捕获所有父作用域中的变量,包括this

  • [&var] : 表示引用传递捕捉变量var

  • [&] : 表示引用传递捕捉所有父作用域中的变量,包括this

  • [this] : 表示值传递方式捕捉当前的this指针

说明:
a. 父作用域指包含lambda函数的语句块
b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割
c. 捕捉列表不允许变量重复捕获
d. 引用捕获可用于修改外部变量,而值捕获却不能实现此操作(mutable 允许修改副本,而不能修改原始变量)
e. 在块作用域以外的lambda捕捉列表必须为空
f. 引用捕获引入生存期依赖项,而值捕获却没有生存期依赖项
g. lambda表达式之间不能相互赋值,哪怕看起来类型一样,因为生成的lambda类名不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>
int x = 1;
auto fun = [x]{ std::cout << x << std::endl; }; // 错误,块作用域以外的lambda捕捉列表必须为空
int main()
{
int a = 3;
int b = 5;

// 按值来捕获某个变量
auto func1 = [a] { std::cout << a << std::endl; };
func1();

// 按值来捕获
auto func2 = [=] { std::cout << a << " " << b << std::endl; };
func2();

// 按引用来捕获某个变量
auto func3 = [&a] { std::cout << a << std::endl; };
func3();

// 按引用来捕获
auto func4 = [&] { std::cout << a << " " << b << std::endl; };
func4();

// 混合捕获
auto func5 = [&,b] { std::cout << a << " " << b << std::endl; };
func5();
auto func6 = [=,b] { std::cout << a << " " << b << std::endl; };
func6();

auto func6 = [&,&b] { std::cout << a << " " << b << std::endl; }; // 错误,&已经引用捕捉作用域所有变量,不能重复对b变量进行引用捕捉
auto func7 = [=,b] { std::cout << a << " " << b << std::endl; }; // 错误,与上类似

auto func8 = [=] {
a = 10; // 错误,按值捕获不能修改
std::cout << a << " " << b << std::endl;
};

auto func8 = [=] mutable{
a = 10; // 正确,但是a只是外部a的值拷贝,对a对修改并不影响外部a变量
std::cout << a << " " << b << std::endl;
};
}

2.2 parameters

参数列表是可选的,它在大多数方面类似于函数的参数列表。

1
2
3
4
5
6
auto fun1 = [] (int first, int second)
{
return first + second;
};

auto fun2 = []{ std::cout << "Hello Worl" << std::endl;};

在 C++14 中,如果参数类型是泛型,则可以使用 auto 关键字作为类型说明符。 此关键字将告知编译器将函数调用运算符创建为模板。 参数列表中的每个 auto 实例等效于一个不同的类型参数。

1
2
3
4
5
6
7
8
int a = 3;
double b = 5.1;

auto func = [](auto x,auto y){
auto ret = x + y;
std::cout << ret << std::endl;
};
func1(a,b);

2.3 mutable

mutable为可选

通常Lambda 的函数调用运算符是 const-by-value,但对 mutable 关键字的使用可将其取消。它不产生 mutable 数据成员。 利用 mutable 规范,Lambda 表达式的主体可以修改通过值捕获的变量

2.4 return-type

return-type为可选的情况

  • statement仅一个返回语句,编译器将从返回表达式的类型推导返回类型
  • statement没有返回值,编译器会将返回类型推导为 void
1
2
3
4
5
auto x1 = [](int i){ return i; }; // OK: return type is int
auto x2 = []{ return{ 1, 2 }; }; // ERROR: return type is void, deducing
// return type from braced-init-list isn't valid

auto x3 = []->vector<int>{ return{ 1, 2}; }; // OK

2.5 statement

statement是一个复合语句。 它可以包含普通函数或成员函数体中允许的任何内容。 普通函数和 lambda 表达式的statement均可访问以下变量类型:

  • 从封闭范围捕获变量,如前所述
  • 参数
  • 本地声明变量
  • 类数据成员(在类内部声明并且捕获 this 时)
  • 具有静态存储持续时间的任何变量(例如,全局变量)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

int i = 1;
static int j = 2;

int main()
{
/* 可以在lambda 表达式体中使用具有静态持续存储时间的变量 */
auto f = []{
i = 3; // 访问的就是全局变量i
j = 4; // 访问的就是静态全局变量j
std::cout << i << std::endl;
std::cout << j << std::endl;
};
f();
std::cout << i << std::endl; // 输出 3
std::cout << j << std::endl; // 输出 4

return 0;
}

三、lambda与函数对象

​ 在lambda出现之前,编写代码有时候需要用到函数指针或函数对象。它们各有利弊,例如:函数指针具有最低的语法开销,但不保持范围内的状态,函数对象可保持状态,但需要类定义的语法开销。

​ lambda 结合了函数指针和函数对象的优点并避免其缺点。 与函数对象一样,lambda 是灵活的并且可以保持状态,但与函数对象不同之处在于其简洁的语法不需要显式类定义。 使用 lambda,可以编写出比等效的函数对象代码更简洁、更不容易出错的代码。

四、lambda表达式示例

4.1 编译器对lambda处理

C++ 编译器将在声明而非调用 lambda 表达式时,将表达式绑定到捕获的变量。 以下示例显示一个通过值捕获局部变量 i 并通过引用捕获局部变量 j 的 lambda 表达式。 由于 lambda 表达式通过值捕获 i,因此在程序后面部分中重新指派 i 不影响该表达式的结果。 但是,由于 lambda 表达式通过引用捕获 j,因此重新指派 j 会影响该表达式的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

int main()
{
using namespace std;

int i = 3;
int j = 5;

auto f = [i, &j] { return i + j; };

i = 22;
j = 44;

cout << f() << endl; // 输出 47
}

4.2 lambda调用

可以立即调用 lambda 表达式,也可以将lambda作为参数传递给函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

int main()
{
int n = [] (int x, int y) { return x + y; }(5, 4); // 立即调用
cout << n << endl; // 输出: 9

vector<int> numbers{3,5,8,13,14,15};
const vector<int>::const_iterator result = find_if(numbers.begin(),
numbers.end(),
[](int n) {
return (n % 2) == 0; }); // 作为函数参数

// 输出: The first even number in the list is 8.
if (result != numbers.end()) {
cout << "The first even number in the list is " << *result << "." << endl;
} else {
cout << "The list contains no even numbers." << endl;
}

return 0;
}

4.3 嵌套lambda

可以将 lambda 表达式嵌套在另一个中。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
using namespace std;

int main()
{
int timestwoplusthree = [](int x) {
return [](int y) {
return y * 2;
}(x) + 3;
}(5);

cout << timestwoplusthree << endl; // 输出: 13
}

五、lambda底层实现

lambda表达式本质是编译器将其翻译成一个类,并重载 operator()来实现。

lambda的各个部分和类的各个部分对应关系:

  • capture-list <-> 类中private成员
  • parameters <-> 类中operator()成员函数的形参列表
  • mutable <-> 类成员函数 operator() 的const属性 (只有在捕获列表捕获的参数不含有引用捕获的情况下才会生效)
  • return-type <-> 类成员函数 operator() 的返回类型
  • statement <-> 类成员函数 operator() 的函数体

说明:编译器生成的lambda类型一般为lambda_ + uuid ,确保类名是唯一的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <algorithm>
#include <string>
#include <vector>

using Date = std::string;

struct Person {
std::string name;
Date birthday;
};

std::vector<Person> peoples{{"Marie Curie", "1867-11-7"},
{"Albert Einstein", "1879-3-14"},
{"Johann Carl Friedrich Gauß", "1777-4-30"}};

void sortPeopleNoLambda() {
// 按照姓名排序
struct ByName {
bool operator()(const Person& a, const Person& b) {
return a.name < b.name;
}
};
std::sort(peoples.begin(), peoples.end(), ByName{});
}

void sortPeopleWithLambda() {
std::sort(peoples.begin(), peoples.end(), [](const Person& a, const Person& b){
return a.name < b.name;
});
}

/* 编译器会将lambda转化为 */
[](const Person& a, const Person& b){
return a.name < b.name;
}


struct __lambda_1 /* 不要在意名字 */ {
inline bool operator(const Person& a, const Person& b) const {
a.name < b.name;
}

__lambda_1() = delete; // 没有默认构造函数
__lambda_1& operator=(const __lambda_1&) = delete; // 不能赋值
};
__lambda_1(); // 对象实例编译器自动创建的,所以不会报错

5.1 变量捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// 传值捕获
int i = 0;
int j = 0;

auto f = [=] {
return i == j;
}

/* 编译器会把上面的lambda表达式代码转为:*/
struct __lambda_2 {
__lambda_2(int i, int j): __i(i), __j(j) {}

inline bool operator()() const {
return __i == __j;
}

private:
int __i;
int __j;
};

__lambda2 f(i,j);
bool ret = f();

// 引用捕获:

int i = 0;
int j = 0;

auto f = [&] {
return i == j;
}
lambda 表达式会转为:

struct __lambda_2 {
__lambda_2(int& i, int& j): __i(i), __j(j) {}

inline bool operator()() { // 没有const修饰
return __i == __j;
}

private:
int& __i;
int& __j;
};

__lambda2 f(i,j);
bool ret = f();

// this 捕获:

struct X {
void printAsync() {
callAsync([this] {
// 可以使用 X 类里的成员
std::print("X::i={}", i);
});
}

private:
int i{42};
};
编译器把 lambda 转为:

struct X {
void printAsync() {
struct __lambda_3 {
__lambda_3(X* _this): __this(_this) {}
void operator()() const {
std::print("X::i={}", __this->i);
}
private:
X* __this;
};
}

callAsync(__lambda_3(this));

private:
int i{42};
};