前言
C++11后,lambda表达式被引入,它常常用来创建临时的匿名可调用对象,以简化代码编写。
本文主要从两个方面来剖析lambda表达式:
- lambda语法
- lambda底层实现
lambda语法
lambda的语法形式如下:
[capture list](parameter list) -> return type {function body}
其中,参数列表和返回类型是可以省略的,但必须永远包含捕获列表和函数体。
一些例子:
auto f = [] {return 42;}
[](const string& lhs, const string& rhs) {return a.size() < b.size();}
[a](int b) -> bool {return b < a;}
lambda表达式可以被作为一个变量来保存,此变量的类型只能用auto声明。
C++14之后,lambda的参数列表部分可以用auto声明,形参类型将被自动推导:
[](const auto& value) {return value % 3 == 0;}
C++14之前,仅当lambda表达式无返回语句或只有一条返回语句时,其返回值才可被自动推导。
C++14之后,只要返回类型兼容即可自动推导。
捕获后的变量才能在lambda函数体中使用,而能捕获变量也是lambda和普通函数的一大区别,众所周知,普通函数只能使用来自于参数或全局定义的变量,而lambda表达式可以在声明时捕获局部变量,以供lambda函数体使用。
那么,lambda如何捕获变量?有哪些不同的捕获方式呢?
捕获形式
捕获能且只能应用于当前作用域中的非静态局部变量(包括形参),外部的静态存储期变量虽然不能被捕获,但可以被直接使用。见例:
int a = 1;
void func() {
static int b = 2;
int c = 3;
auto f = [/*capture list*/]() { // 只能捕获自动存储期的局部变量c,而不能捕获静态存储期的变量a,b
// use a variable // a, b不经捕获也能使用
};
f();
}
捕获可以分为两种:
- 值捕获,将被捕获变量的值拷贝到lambda中
- 引用捕获,在lambda中保存被捕获变量的引用(指针)
另外还有隐式捕获,即单独的&,=声明而不指定变量名,那么lambda函数体中用到的未显式捕获的变量将以被隐式地(引用/值)捕获。
捕获列表的形式汇总:
-
默认捕获:[],什么也不捕获
-
值捕获:[varname],如[a],按值捕获被声明的变量
-
引用捕获:[&varname],如[&a],按引用捕获被声明的变量
-
全值捕获:[=],隐式按值捕获所有用到的变量
-
全引用捕获:[&],隐式按引用捕获所有用到的变量
-
混合使用:[&, varname1…],如[&, a, b],出现在列表中的变量被值捕获,而隐式捕获的变量被引用捕获。
-
混合使用:[=, &varname1…],如[=, &a, &b],出现在列表中的变量被引用捕获,而隐式捕获的变量被值捕获。
注意=只在全值捕获或混合捕获中单独出现,而不能直接修饰某个被捕获变量,换言之,一个不被&修饰的变量默认就是值捕获的
关于this指针的捕获
在一个类的方法中定义的lambda表达式不能直接捕获字段,因为字段不是当前作用于内的局部变量,但是可以捕获this指针,然后就可以使用类中的字段:
class hello
{
public:
void func()
{
auto f = [this]()
{
// d其实是this->d
cout << d << endl;
};
f();
}
private:
int d = 4;
};
C++20之后不允许this指针的隐式捕获,即C++20之前,要捕获this指针,可以使用以下写法:
[=]{};
[this]{};
C++20之后,只允许下面的写法:
[=, this]{};
[this]{};
空悬问题
使用引用捕获时,lambda表达式将包含指涉到局部变量的引用,那么当lambda表达式的生命期超过该局部变量的生命期时,此lambda表达式将有一个空悬的无效引用,见下面这个致命的例子:
std::vector<std::function<bool<int>>;
void addFilter() {
auto divisor = getDivisor();
filters.emplace_back([&divisor](int value) {
return value % divisor == 0;
});
// 当前函数返回后,divisor的值将失效,后续对该filter的使用将出错!
}
捕获this指针时也会有空悬问题,当lambda表达式作用于超过了当前对象的生存期后,this指针就会空悬。
mutable
lambda表达式按值捕获的变量默认都是不可修改的(当然,按引用捕获或者静态存储期变量可以修改),可以理解作它们被作为const字段保存在lambda实例中。见下面的例子:
int a = 1;
void func() {
int b = 2;
int c = 3;
auto f = [b, &c] {
a = 10; // OK
// b = 20; // Compile Error: assignment of read-only variable 'b'
c = 30; // OK
};
cout << a << " " << b << " " << c << endl;
}
输出如下:
10 2 30
要在lambda函数体中修改按值捕获的变量,就要加上关键字mutable:
auto f = [b, &c] () mutable {
a = 10; // OK
b = 20; // OK
c = 30; // OK
};
输出仍是10 2 30
,这是因为即使mutable令b可修改,但修改的是lambda中的副本b(修改结果只在lambda函数体中可见),所以原来的局部变量b的值不受影响。
广义捕获
广义捕获又叫初始化捕获,自C++14引入,它为移动捕获提供了直接支持(之前的移动捕获需要借助std::bind笨拙实现)。
广义捕获形式非常简单,就是在捕获列表中定义lambda中的新变量,例如:
auto pw = std::make_unique<Widget>();
[p = std::move(pw)] { // = 后面可以接任何合法的表达式,p变量将自动成为lambda内置的字段
// do sth with p
};
本质
本质上,lambda是一个函数对象,大多编译器实现会把lambda的定义转换为一个类定义,被捕获的变量是类的字段,而lambda函数体就是类的operator()
方法体。
举例,以下的lambda表达式:
size_t sz = getSz();
auto f = [sz](const string& a) {return a.size() >= sz;}
可能会被转换为以下类:
class f {
public:
f(size_t n) : sz(n) {}
bool operator()(const string& a) const {
return s.size() >= sz;
}
private:
const size_t sz;
};
那么:
- 按值捕获或按引用捕获的对应类字段的声明是值类型或引用类型。
- 有无mutable修饰对应类字段和方法有无const修饰
引用
《C++Primer 5e》
《Effective Modern C++》