C++ Lambda 学习笔记

C++ 中,对于一个对象或一个表达式,如果可以对其使用调用运算符(()),则称它是可调用的。即,如果 e 是可调用的,则可以这样使用:

1
e(args)

其中,args 是一个逗号分隔的一个或多个参数的列表。

C++ 中可调用对象除了我们熟悉的函数或函数指针外,还包括函数对象以及 lambda 表达式。

本文重点讲述 lambda 表达式。

lambda 表达式表示一个可调用的代码单元,我们可以将其理解为一个未命名的内联函数。一个 lambda 表达式具有如下形式:

1
[capture list](parameter list) -> return type {function body}
  • capture list:捕获列表,lambda 表达式所在函数中定义的局部变量列表
  • parameter list:参数列表
  • return type:返回类型
  • function body:函数体

lambda 表达式可以忽略参数列表和返回类型,但必须包含捕获列表和函数体:

1
auto f = [] {return 10;};

上述代码中,我们定义了一个可调用对象 f,它不接受参数,返回 10。

lambda 的调用方式与普通函数的调用方式一样:

1
cout << f() << endl;

输出:

10

向 lambda 传递参数

lambda 可以带上参数,例如:

1
2
3
[](const string &s1, const string &s2) {
return s1.size() < s2.size();
};

上述定义的 lambda 带上两个 const string & 类型的参数,lambda 函数体比较两个 string 的长度并返回 bool 值。

可以使用此 lambda 来调用 std::stable_sort 排序。当 std::stable_sort 需要比较两个元素时,它就会调用指定的这个 lambda。

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

using namespace std;


int main() {
vector<string> words{"leehao.me", "lee", "hao", "leehao"};
std::stable_sort(words.begin(), words.end(),
[](const string &s1, const string &s2) {
return s1.size() < s2.size();
});

for (const auto &word : words) {
cout << word << endl;
}

return 0;
}

输出:

lee
hao
leehao
leehao.me

lambda 捕获列表

一个 lambda 可以使用其所在函数中的局部变量,但需要明确指明这些变量。lambda 捕获列表可以用来声明这些需要使用的局部变量。例如:

1
2
3
[sz](const string &s) {
return s.size() >= sz;
};

由于此 lambda 捕获 sz,因此,可以在函数体中使用 sz。如果未捕获 sz,则会报编译错误:

1
2
3
4
// 编译错误:sz 未捕获
[](const string &s) {
return s.size() >= sz;
};

lambda 本质上是函数对象。当向一个函数参数传递一个 lambda 时,同时定义了一个新的函数对象类型并生成一个此类型的未命名对象。

因此,从 lambda 生成的函数对象类型包含此 lambda 所捕获的变量的数据成员,且 lambda 的数据成员在 lambda 对象创建时被初始化。

值捕获

类似参数传递,变量的捕获方式可以是值或引用。与传递参数类似,采用值捕获的前提是变量可以拷贝。与参数不同,被捕获的变量的值是在 lambda 创建时拷贝,而不是调用时拷贝:

1
2
3
4
5
6
7
8
9
void fcn1()
{
size_t v1 = 42; // 局部变量
// 将 v1 拷贝到名为 f 的可调用对象
auto f = [v1] {return v1;};
v1 = 0;
auto j = f(); // j 为 42,f 保存了 lambda 创建时 v1 的拷贝,即 42
cout << j << endl;
}

输出:

42

由于被捕获变量的值是在 lambda 创建时拷贝,因此,随后对其修改不会影响到 lambda 内对应的值。

引用捕获

定义 lamda 时可以采用引用方式捕获变量:

1
2
3
4
5
6
7
8
9
void fcn2()
{
size_t v1 = 42; // 局部变量
// 对象 f2 包含 v1 的引用
auto f2 = [&v1] {return v1;};
v1 = 0;
auto j = f2(); // j 为 0,f2 保存 v1 的引用,而非拷贝
cout << j << endl;
}

输出:

0

v1 前的 & 表示 v1 以引用方式捕获。当我们在 lambda 函数体内使用此变量,实际上使用的是引用所绑定的对象。因此,当 v1 在修改后,对象 f2 引用的 v1 也发生修改。

当以引用方式捕获一个变量时,必须保证在 lambda 执行时,变量是存在的。

lambda 返回类型

上述例子中,我们并没有为 lambda 指定返回类型,这是由于编译器可以正常推断出 lambda 的返回类型。

例如,使用标准库算法 transform 和一个 lambda 来将整型向量中每个负数转化为其绝对值:

1
2
3
vector<int> vi {1, -2, 3, 2, -4, 5};
std::transform(vi.begin(), vi.end(), vi.begin(),
[](int i) {return i < 0 ? -i : i;});

transform 第 4 个参数是一个 lambda。
lambda 函数体是单一的 return 语句,我们没有指定返回类型,这是由于编译器可以根据条件运算符来推断出返回类型。

也可以指定 lambda 的返回类型:

1
2
3
4
vector<int> vi {1, -2, 3, 2, -4, 5};
std::transform(vi.begin(), vi.end(), vi.begin(),
[](int i) -> int
{if (i < 0) return -i; else return i;});

transform 第 4 个参数是一个 lambda,它的捕获列表为空,接受一个 int 参数,返回一个 int 值。

可以输出 vi 元素的值:

1
2
3
for (auto i : vi) {
cout << i << endl;
}

输出:

1
2
3
2
4
5

参考资料

  • 《C++ Primer》,第五版,中文版