Lambda 表达式,也称为匿名函数、闭包函数,在别的编程语言很早就有了。
C++ 11 开始,也支持了这个功能。而后续的 C++ 版本又陆陆续续做了些改进。
整理了编笔记,把 lambda 表达式的用法试验、记录一下。
lambda 表达式的语法如下:
[ captures ] ( params ) specs -> return-type { body }
复制代码
captures:捕获列表,用来捕获当前作用域的变量,然后可以在 lambda 表达式内部使用。支持按值捕获、按引用捕获。用法和行为,跟普通的 C++函数调用很像。
params:参数列表,可选。在调用 lambda 表达式时,额外传递的参数。
specs:限定符,可选。比如说 mutable,后面的试验会用到它。在后续的 C++ 17、20、23 等版本,还添加了 constexpr、consteval、static 这些。
return-type:返回类型,可选。
body:函数体
整个看起来,跟普通的 C++函数很像:
就是函数的函数名,在 lambda 里面不需要定义。所以也把 lambda 表达式称为匿名函数。
这是一个简单的 lambda 表达式例子:
#include <iostream>
int main()
{
int x = 1;
// Define a simple lambda
auto add_func = [x] (int y) {
return x + y;
};
// Call lambda
int ans = add_func(10);
std::cout << ans << std::endl;
return 0;
}
复制代码
运行结果很浅白,会输出:11
add_func 表达式在定义时,把作用域的 x 变量捕获,然后其函数体内使用 x、和额外传递的参数 y 进行运算,最后返回计算结果。
那么我们把代码稍作修改:
#include <iostream>
int main()
{
int x = 1;
// Define a simple lambda
auto add_func = [x] (int y) {
return x + y;
};
// Call lambda
int ans = add_func(10);
std::cout << ans << std::endl;
// Modify x outside
x = 2;
// Call lambda again
ans = add_func(10);
std::cout << ans << std::endl;
return 0;
}
复制代码
在第一次调用 add_func 之后,我们把外部的 x 变量修改了,然后,再次调用 add_func。
运行这个代码,会发现输出:
11
11
外部 x 变量的修改,并没有传递到 lambda 内部。
这是因为:C++在编译期间,编译器自动为 lambda 表达式生成一个闭包 ClosureType 类。在 lambda 表达式被定义的地方,实例化该类,生成实例 add_func,并对被其捕获的成员变量进行赋值:
所以,按值捕获的变量,在 lambda 定义时,它在 lambda 内部的值已经被确定下来。后续外部对变量 x 的修改,不会再影响到 lambda 内部的__x。
那么,如果需要修改按值捕获的变量,应该怎么做呢?修改完以后,lambda 内外的变量会发生什么变化呢?
#include <iostream>
int main()
{
int x = 1;
// Define lambda to modify value captured
auto modify_func = [x] () {
x++;
};
return 0;
}
复制代码
像这样,直接对按值捕获的变量进行修改,编译器会报错:
error: increment of read-only variable 'x'
x++;
复制代码
需要用到一开始说的 mutable 限定符,改为这样就可以了:
auto modify_func = [x] () mutable {
复制代码
加上一些输出信息之后,代码变成了这样:
#include <iostream>
int main()
{
int x = 1;
// Define lambda to modify value captured
auto modify_func = [x] () mutable {
std::cout << "x inside lambda is: " << x << std::endl;
x++;
};
std::cout << "Before calling lambda, x out of lambda is: " << x << std::endl;
modify_func();
std::cout << "After calling lambda, x out of lambda is: " << x << std::endl;
modify_func();
std::cout << "After calling lambda again, x out of lambda is: " << x << std::endl;
return 0;
}
复制代码
运行这段代码,可以得到这些输出:
Before calling lambda, x out of lambda is: 1
x inside lambda is: 1
After calling lambda, x out of lambda is: 1
x inside lambda is: 2
After calling lambda again, x out of lambda is: 1
复制代码
这里可以看出两个信息:
按值捕获之后,lambda 内外的变量已经没有关系,各自有各自的数值。
修改 lambda 实例的成员变量之后,该修改会一直生效,直到 lambda 实例的生命周期结束。
第一点信息,前面已经解释过。第二点信息,跟第一点信息的原理也密切相关。
可以这么理解,闭包 ClosureType 类的实例 modify_func,根据捕获的变量,内部相应创建了成员变量__x。Lambda 内部的 x++,其实是 modify_func.__x++。所以,下次再次调用 modify_func 时,其成员变量__x 保留了上次调用的数值。
以上两点信息,只要理解了 lambda 表达式其实是个 ClosureType 类,由编译器根据捕获的变量,自动生成对应的成员变量。然后在 lambda 表达式定义的地方被实例化、初始化成员变量。而后的 lambda 表达式调用,本质上是调用了该实例的成员函数。那么这些行为就很自然而然了。
接下来,按引用捕获变量。
按引用捕获,用法、行为跟普通函数的按引用传递没什么区别。只需要在捕获的变量前,加上 &符号即可。
#include <iostream>
int main()
{
int x = 1;
// Define lambda to capture by reference
auto ref_func = [&x] () {
std::cout << "x inside lambda is: " << x << std::endl;
x++;
};
std::cout << "Before calling lambda, x out of lambda is: " << x << std::endl;
ref_func();
std::cout << "After calling lambda, x out of lambda is: " << x << std::endl;
x = 5;
std::cout << "Now change x out of lambda to: " << x << std::endl;
ref_func();
std::cout << "After calling lambda again, x out of lambda is: " << x << std::endl;
return 0;
}
复制代码
x 变成 &x,变成了按引用捕获变量,之后,lambda 内部和外部,共享同一个变量。一方的修改,将反应到另一方上面。
所以,上面的代码将输出:
Before calling lambda, x out of lambda is: 1
x inside lambda is: 1
After calling lambda, x out of lambda is: 2
Now change x out of lambda to: 5
x inside lambda is: 5
After calling lambda again, x out of lambda is: 6
复制代码
另外,如果需要按值捕获外部的所有变量,通过[=]即可。
而通过[&],可以按引用捕获外部的所有变量。
最后一点,针对外部的全局变量或者局部 static 变量,可以在 lambda 表达式内部直接使用、修改;内外共享一个变量。比如下面的代码:
#include <iostream>
int global_val = 1;
int main()
{
// Define lambda to use global param
auto global_func = [] () {
std::cout << "global_val inside lambda is: " << global_val << std::endl;
global_val++;
};
std::cout << "Before calling lambda, global_val out of lambda is: " << global_val << std::endl;
global_func();
std::cout << "After calling lambda, global_val out of lambda is: " << global_val << std::endl;
global_val = 5;
std::cout << "Now change global_val out of lambda to: " << global_val << std::endl;
global_func();
std::cout << "After calling lambda again, global_val out of lambda is: " << global_val << std::endl;
return 0;
}
复制代码
这段代码将输出:
Before calling lambda, global_val out of lambda is: 1
global_val inside lambda is: 1
After calling lambda, global_val out of lambda is: 2
Now change global_val out of lambda to: 5
global_val inside lambda is: 5
After calling lambda again, global_val out of lambda is: 6
复制代码
可以看到,不用显式捕获全局变量,lambda 表达式内部可以直接使用;lambda 内部和外部,共享同一个全局变量。一方的修改,将反应到另一方上面。
综上所述,lambda 表达式有这些特点:
按值捕获的变量,在 lambda 定义时,它在 lambda 内部的值已经被确定下来。之后,外部对该变量的修改,不会再影响到 lambda 内部的那一份。
在 lambda 内部,修改捕获的变量之后,该修改会一直生效,直到 lambda 实例的生命周期结束。
按引用捕获的变量,lambda 内部和外部,共享同一份。一方的修改,将反应到另一方。
不用显式捕获全局变量,lambda 表达式内部可以直接使用;lambda 内部和外部,共享同一份全局变量。一方的修改,将反应到另一方。
掌握了这些知识,就足以满足常见的 lambda 表达式应用了。
评论