C++ Lambda表达式中的捕获列表


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] 表示 ab 按引用,其余按值捕获
[&, identifier_list]identifier list里的变量按值捕获,其余的变量按引用捕获,比如 [&, a, b] 表示 ab 按值,其余按引用捕获

3 小结

这篇文章结合了几个简单的示例小结了C++ Lambda表达式中不同的捕获方式的区别和用法,不过这里只是简单地把最基础的部分总结了一下,在真实的程序中它的用法远更加复杂多样,但至少今后再遇到lambda的捕获列表的时候就知道它代表的意思和作用了。

下面说的是题外话。今天是我从业以来第一次写技术相关的博客,主要原因是最近学习的内容特别多,输入很多但输出却很少,常常觉得囫囵吞枣,看了很多却消化理解得很少。而写博客就是一种输出,把刚刚学习的新鲜内容细嚼慢咽成自己的理解再写成文章,输入输出成为闭环,对我自身学习有很大的好处,也可以为技术社区提供可能并不多但也有价值的学习资料。今后要学习的东西还很多,所以很希望自己能够将这个习惯保持下去。

  • 32
    点赞
  • 57
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
在 Lambda 表达式捕获列表用于指定要捕获的外部变量。捕获列表可以包含以下 3 种方式: 1. 捕获捕获值可以通过在捕获列表指定变量的方式来完成。在捕获值时,Lambda 表达式会在创建时将指定的变量复制一份,并在函数体使用这份副本。例如: ```c++ int x = 1; auto func = [x]() { std::cout << x << std::endl; }; ``` 在上面的代码,Lambda 表达式通过捕获值的方式来捕获变量 x。在 Lambda 表达式使用的变量 x 是一个拷贝,对其进行修改不会影响原有的变量 x。 2. 捕获引用 捕获引用可以通过在捕获列表指定变量的方式来完成。在捕获引用时,Lambda 表达式会在创建时将指定的变量的引用传递给函数体。例如: ```c++ int x = 1; auto func = [&x]() { std::cout << x << std::endl; }; ``` 在上面的代码,Lambda 表达式通过捕获引用的方式来捕获变量 x。在 Lambda 表达式使用的变量 x 是原有变量的引用,对其进行修改会影响原有的变量 x。 3. 捕获列表 捕获列表可以通过在捕获列表指定变量的方式来完成。在捕获列表,可以同时指定多个变量,并且可以使用值或引用的方式进行捕获。例如: ```c++ int x = 1, y = 2; auto func = [x, &y]() { std::cout << x << " " << y << std::endl; }; ``` 在上面的代码,Lambda 表达式通过捕获列表的方式来捕获变量 x 和 y。变量 x 是以值的方式进行捕获的,变量 y 是以引用的方式进行捕获的。 需要注意的是,在 Lambda 表达式捕获变量时,要考虑变量的生命周期。如果捕获了一个局部变量,而且这个局部变量已经超出了作用域,那么在 Lambda 表达式执行时可能会引发未定义的行为。为了避免这种情况,可以使用捕获值或捕获引用的方式来避免变量超出作用域的问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值