概述
文章目录
- 0 前言
- 1 Lambda表达式简介
- 2 捕获列表(Capture List)
- 2.1 按值捕获
- 2.2 按引用捕获
- 2.3 隐式捕获
- 3 小结
0 前言
最近通过内部换组来到了新的开发组,组里核心业务用的是C++17,并且十分注重性能和资源的优化。我作为一个C++小白,趁着年底没有太多工作、下一年的项目也还没有启动的空档,看代码之外也花了些时间阅读C++书籍,希望能系统地学习这门语言。
最近在看项目代码时发现Lambda表达式在我们的代码中很常见,正好今天在阅读C++ Primer的时候读到了C++ Lambda的介绍,它在用法上和Java很相似,但有一个要素在Java的lambda表达式中很少特意提出来讲,那就是变量的捕获。这篇文章主要是想简单地总结一下C++ Lambda表达示的捕获列表的概念和用法。
1 Lambda表达式简介
在C++在11及之后的版本中支持了lambda表达式,它是一种像函数一样可调用的对象,就和匿名的inline function差不多。一个简单的lambda表达式的主要由返回类型、参数列表、捕获列表和方程体组成,可以表示为
[capture list] (parameter list) -> return type { function body }
关于Lambda表达式的更多内容,我会在以后的文章中多写学习笔记和自己的理解,这里先不赘述,先来看看捕获列表这个概念。
2 捕获列表(Capture List)
尽管Lambda表达式常常会被用在一个函数里,但它只能访问外部的全局变量而并不能访问函数里的局部变量,而这个捕获列表就是这Lambda表达式提供一个函数内的局部变量列表,只要被列在了列表内,那么表达式都可以对其进行访问。Lambda会将这些变量信息和自身放在一起,通常我们把它们这个整体叫作闭包(enclosure)。
一般来说,捕获变量的方式主要有两种:按值捕获(capture by value)和按引用捕获(capture by reference),不同的捕获方式不仅仅是让lambda表达式访问变量的方式不同,还由于它们在编译上的差别,它们对程序的限制也有所不同。这里为了方便也为了和C++ Primer的讲述方式一致,我将它们分为三大类来讲:
- 按值捕获(capture by value)
- 按引用捕获(capture by reference)
- 隐式捕获(implicit captures)
2.1 按值捕获
和传递变量里的传递变量值类型相似,这种捕获方式捕获的也是变量的值,在编译期间,会在Lambda表达式被创建的时候把这个变量复制给局部的一个临时变量,所以这种捕获方式的变量必须是可以被复制的变量。比如在以下的例子中,我们将函数内的整型变量v1
按值放入捕获列表中来让Lambda表达式能够访问其变量值:
#include <iostream>
using namespace std;
int main() {
int v1 = 1;
auto f = [v1] { return v1; };
v1 = 2;
std::cout << f() << endl;
return 0;
}
由于我们是按值捕获,lambda在创建时将v1
此刻的值复制到局部临时变量中,即使事后v1
的值有所改变,lambda不会有所察觉,它只会保存它创建的时刻的v1
的值。因此这段代码的输出为:
1
2.2 按引用捕获
按引用捕获可以让lambda表达式能够访问引用变量,这种捕获方式在编译期间不会把变量的值复制到lambda的局部作用域,而是直接对这个引用所指的变量进行操作。这种方式要求这个引用所指的对象在表达式被执行的时候必须是存在的。例如以下这段代码中,我们尝试将函数内的局部变量v1
以引用的方式作为捕获变量让lambda访问:
#include <iostream>
using namespace std;
int main() {
int v1 = 1;
auto f = [&v1] { return v1; };
v1 = 2;
std::cout << f() << endl;
return 0;
}
因为这是按引用捕获,lambda在访问时也将直接访问这个引用,也就是函数内的&v1
,因此即使在lambda被创建之后v1的值有所改变,lambda也依旧是可以直接访问这个引用变量,它的输出将是:
2
值得一提的是,按引用捕获在实际应用中是有风险的,主要原因是lambda表达式在被创建后到被执行前这段期间里,这个引用有可能会变得不复存在了,这时候就很难保证这个引用变量在执行的时候一定存在。所以除非我们能确保它在lambda执行的时候是一定存在的,否则应该尽量避免用按引用捕获。
既然按引用捕获有风险,那它有没有存在的必要呢?也有,比如当我们想捕获的局部作用域变量是一个不可复制的变量的时候,就只能通过引用的方式来捕获了。以下的例子中,我们想让lambda可以访问ostream对象,但是它是不可复制的,于是我们将其引用给lambda:
#include <iostream>
using namespace std;
int main() {
ostream &os = cout;
auto f = [&os] { os << "hello"; };
f();
os << endl;
return 0;
}
输出:
hello
但如果我们尝试将os
按值捕获,则在编译时报错:
❯ g++ -std=c++11 -o lambda lambda.cpp
lambda.cpp:7:22: error: invalid operands to binary expression ('const std::__1::ostream' (aka 'const basic_ostream<char>') and 'const char [6]')
auto f = [os] { os << "hello"; };
~~ ^ ~~~~~~~
2.3 隐式捕获
除了直接地向lambda指定捕获方式之外,常见地,还可以用以下几种方式来告诉lambda它可以捕获的变量:
捕获列表 | 说明 |
---|---|
[] | 不可访问函数内的局部变量,只可访问全局变量 |
[=] | 函数局部作用域里的所有变量都按值捕获 |
[&] | 函数局部作用域里的所有变量都按引用捕获 |
[=, reference_list:] | reference list里的变量按引用捕获,其余的变量按值捕获,比如 [=, &a, &b] 表示 a 和 b 按引用,其余按值捕获 |
[&, identifier_list] | identifier list里的变量按值捕获,其余的变量按引用捕获,比如 [&, a, b] 表示 a 和 b 按值,其余按引用捕获 |
3 小结
这篇文章结合了几个简单的示例小结了C++ Lambda表达式中不同的捕获方式的区别和用法,不过这里只是简单地把最基础的部分总结了一下,在真实的程序中它的用法远更加复杂多样,但至少今后再遇到lambda的捕获列表的时候就知道它代表的意思和作用了。
下面说的是题外话。今天是我从业以来第一次写技术相关的博客,主要原因是最近学习的内容特别多,输入很多但输出却很少,常常觉得囫囵吞枣,看了很多却消化理解得很少。而写博客就是一种输出,把刚刚学习的新鲜内容细嚼慢咽成自己的理解再写成文章,输入输出成为闭环,对我自身学习有很大的好处,也可以为技术社区提供可能并不多但也有价值的学习资料。今后要学习的东西还很多,所以很希望自己能够将这个习惯保持下去。
最后
以上就是冷傲抽屉为你收集整理的C++ Lambda表达式中的捕获列表0 前言1 Lambda表达式简介2 捕获列表(Capture List)3 小结的全部内容,希望文章能够帮你解决C++ Lambda表达式中的捕获列表0 前言1 Lambda表达式简介2 捕获列表(Capture List)3 小结所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复