C++ 11 引入 lambda 之后,可以很方便地在 C++ 中使用匿名函数,这篇文章主要聊聊其背后的实现原理以及有反直觉的变量捕获机制。在阅读本文之前,需要读者对 C++ lambda 有一个简单的了解。
[capture_list](parameter_list) -> return_type {function_body}
其中,capture_list 表示捕获列表,parameter_list 表示函数参数列表,return_type 表示函数返回类型,function_body 表示函数体。下面是一个简单的 Lambda 函数示例,这里定义一个计算面积的名为 area 的 lambda。
#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 到了哪里呢?
我们使用 C++ insights 来看一下内部可能的实现:
实际编译器会为每一个 lambda 生成唯一的类(functor),有以下的特点:
operator()
所以是可以直接当成函数调用的,函数参数和返回值和 lambda 中声明的完全一致。ps: 其实也可见 C++ 中 lambda 的实现和 Java 的 lambda 转换为匿名内部类的实现,以及 Objective-C 的 block 的实现原理和变量捕获机制都非常的相似。
如果我们将上例中的 area lambda 改成下面会如何?
auto area = [=](double radius) -> double {
pi *= 2;
return pi * radius * radius;
};
实际上编译会失败,clang 会报以下错误:
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 的值。这一点稍微有点违反直觉,需要注意。
也即是说编译器意欲生成的代码是这样的,但发现不合法:
public:
inline /*constexpr */ double operator()(double radius) const
{
pi *= 2;
return (pi * radius) * radius;
}
private:
double pi;
那如何把 const 去掉,使得 lambda 内可以修改捕获的值呢?
答案就是 mutable
关键字,增加 mutable
之后:
auto area = [=](double radius) mutable -> double {
pi *= 2;
return pi * radius * radius;
};
再来看看生成后的 operator()
, 没有了 const,也可以正常修改 this 的属性 pi
public:
inline /*constexpr */ double operator()(double radius)
{
pi = pi * 2;
return (pi * radius) * radius;
}
private:
double pi;
捕获方法分为两种 = 和 &,分别对应 capture by-copy
和 capture by-reference
, 基本的部分这里我们不多做介绍。需要注意的是对 this 的捕获,通过 [&]
和 [=]
对 this 的隐式捕获,以及 [this]
显式捕获都是 by-reference
的,其实捕获的都是 this 指针。
#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]
生成的代码都是完全一致的,如下:
捕获 this 指针 by-refernce 的好处是减少内存的 copy,但处理不当的话,比如 this 指针的生命周期如果没有 lambda 长,那么就会访问的野指针,导致 crash。这种 case 下,可以考虑通过 [*this]
的方式,copy this 对象到 lambda 中。 ps: [*this]
是 C++ 17 引入的。
方框的位置是和上面 by-reference
不同之处,会调用 Math 的 copy 构造创建一个 copy 保存到 lambda 对象中。
需要注意的是,即便是 copy 一份,因为生成的 operation () 还是 const 的,所以并不能修改 Math 的属性,如果需要修改,需要加上 mutable 关键字。
实际场景中,应该根据实际的需要(主要考虑生命周期),来选择是使用 by-copy
还是 by-reference
来捕获 this.
operator()
的 class),并把 capture 的变量作为该类的属性operator()
是 const,如果需要修改 capture 的变量副本,需要加 mutable 关键字修饰[=]
[&]
隐式捕获 还是 [this]
显式捕获 this 都是 by-reference
的,只有 [*this]
是 by-copy
的。注意实现的区别,以及如何进行选择。Ref: