前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C++ Lambda 本质 & 变量捕获

C++ Lambda 本质 & 变量捕获

作者头像
JoeyBlue
发布2023-02-21 09:25:33
1.2K0
发布2023-02-21 09:25:33
举报
文章被收录于专栏:代码手工艺人代码手工艺人

C++ 11 引入 lambda 之后,可以很方便地在 C++ 中使用匿名函数,这篇文章主要聊聊其背后的实现原理以及有反直觉的变量捕获机制。在阅读本文之前,需要读者对 C++ lambda 有一个简单的了解。

C++ Lambda 的函数结构

代码语言:javascript
复制
[capture_list](parameter_list) -> return_type {function_body}

其中,capture_list 表示捕获列表,parameter_list 表示函数参数列表,return_type 表示函数返回类型,function_body 表示函数体。下面是一个简单的 Lambda 函数示例,这里定义一个计算面积的名为 area 的 lambda。

代码语言:javascript
复制
#include <iostream>
int main() {
  double pi = 3.14;
  auto area = [=](double radius) -> double {
    return pi * radius * radius;
  };
  std::cout << "area of circle with radius 2.0 : " <<  area(2.0) << std::endl;
}

这里选择了 by-copy (=) 的方法来捕获 pi 这个变量,也就是会复制一份 pi 进到 area lambda 里,那么这个值 copy 到了哪里呢?

Lambda 在编译期的实现

我们使用 C++ insights 来看一下内部可能的实现:

01.png
01.png

实际编译器会为每一个 lambda 生成唯一的类(functor),有以下的特点:

  1. line 6, 生成的类名唯一,不可读,不同编译器生成的名字可能不一样,我们在运行时是无法拿到具体类名的
  2. line 9, 因为有 operator() 所以是可以直接当成函数调用的,函数参数和返回值和 lambda 中声明的完全一致。
  3. line 15, 捕获的变量在这里,会被转化为类该类的属性,并在构造的传入捕获的参数 (line 15 & line 24)

ps: 其实也可见 C++ 中 lambda 的实现和 Java 的 lambda 转换为匿名内部类的实现,以及 Objective-C 的 block 的实现原理和变量捕获机制都非常的相似。

关于 const

如果我们将上例中的 area lambda 改成下面会如何?

代码语言:javascript
复制
auto area = [=](double radius) -> double {
  pi *= 2;
  return pi * radius * radius;
};

实际上编译会失败,clang 会报以下错误:

代码语言:javascript
复制
lambda.cpp:6:8: error: cannot assign to a variable captured by copy in a non-mutable lambda
    pi *= 2;
    ~~ ^
1 error generated.

这里最主要的原因是编译器生成的匿名类的 operator() 都是 const 的,const 在这里修饰 this 指针 (__lambda_5_15 对象的指针),表示 this 不可变,因此不可以修改属性 pi 的值。这一点稍微有点违反直觉,需要注意。

也即是说编译器意欲生成的代码是这样的,但发现不合法:

代码语言:javascript
复制
public:
  inline /*constexpr */ double operator()(double radius) const
  {
    pi *= 2;
    return (pi * radius) * radius;
  }
private:
  double pi;

那如何把 const 去掉,使得 lambda 内可以修改捕获的值呢? 答案就是 mutable 关键字,增加 mutable 之后:

代码语言:javascript
复制
auto area = [=](double radius) mutable -> double {
  pi *= 2;
  return pi * radius * radius;
};

再来看看生成后的 operator(), 没有了 const,也可以正常修改 this 的属性 pi

代码语言:javascript
复制
public:
  inline /*constexpr */ double operator()(double radius)
  {
    pi = pi * 2;
    return (pi * radius) * radius;
  }

private:
  double pi;

变量捕获方式 & 如何捕获 this 指针

捕获方法分为两种 = 和 &,分别对应 capture by-copycapture by-reference, 基本的部分这里我们不多做介绍。需要注意的是对 this 的捕获,通过 [&][=] 对 this 的隐式捕获,以及 [this] 显式捕获都是 by-reference 的,其实捕获的都是 this 指针。

代码语言:javascript
复制
#include <iostream>
using namespace std;

class Math {
public:
  Math(double value): value_(value) {}
	auto square() {
		return [&]() -> double {
			return value_ * value_;
		};
	}
private:
	double value_;
};

int main() {
	Math math(10);
	std::cout << math.square()() << std::endl;
}

return [&]() -> double ... 这里换成 [=] 或者 [this] 生成的代码都是完全一致的,如下:

01.png
01.png

捕获 this 指针 by-refernce 的好处是减少内存的 copy,但处理不当的话,比如 this 指针的生命周期如果没有 lambda 长,那么就会访问的野指针,导致 crash。这种 case 下,可以考虑通过 [*this] 的方式,copy this 对象到 lambda 中。 ps: [*this] 是 C++ 17 引入的。

方框的位置是和上面 by-reference 不同之处,会调用 Math 的 copy 构造创建一个 copy 保存到 lambda 对象中。

01.png
01.png

需要注意的是,即便是 copy 一份,因为生成的 operation () 还是 const 的,所以并不能修改 Math 的属性,如果需要修改,需要加上 mutable 关键字。

实际场景中,应该根据实际的需要(主要考虑生命周期),来选择是使用 by-copy 还是 by-reference 来捕获 this.

回顾 & 总结

  1. lambda 本质上其实就是使用一个匿名的 functor(带有 operator() 的 class),并把 capture 的变量作为该类的属性
  2. lambda 默认生成的 operator() 是 const,如果需要修改 capture 的变量副本,需要加 mutable 关键字修饰
  3. 通过 [=] [&] 隐式捕获 还是 [this] 显式捕获 this 都是 by-reference 的,只有 [*this]by-copy 的。注意实现的区别,以及如何进行选择。

Ref:

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-12-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • C++ Lambda 的函数结构
  • Lambda 在编译期的实现
  • 关于 const
  • 变量捕获方式 & 如何捕获 this 指针
  • 回顾 & 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档