介绍Lambda表达式中的一些细节:避免隐式捕获、最好通过初始化(移动)进行捕获、如何处理右值。
首先先澄清一些术语:
lambda表达式就是一个表达式,如下面代码花括号部分
std::find_if(container.begin(), container.end(), { return 0 < val && val < 10; });
closure闭包是通过lambda表达式创建的对象,闭包持有被捕获的拷贝和引用。lambda通常用于一次使用的场景。但closure通常是可复制的,因此一个lambda表达式可能会对应着多个closure。
auto c1 = [x](int y) { return x * y > 55; }; auto c2 = c1;
closure class闭包类是一个闭包的实现。编译器会为每个lambda表达式生成一个唯一的closure class,lambda表达式中的代码会成为这个类的成员函数的可执行代码。
Item 31:Avoid default capture modes.
先介绍一些基础知识。Lambda表达式的完整声明格式如下:
[capture list] (params list) mutable exception-> return type { function body }
各项含义如下:
- capture list:捕获外部变量列表
- params list:形参列表
- mutable指示符:用来说用是否可以修改捕获的变量
- exception:异常设定
- return type:返回类型
- function body:函数体
明确几个捕获类型:
值捕获
int a = 123; auto f = [a] { cout << a << endl; };
引用捕获
int a = 123; auto f = [&a] { cout << a << endl; };
隐式捕获
隐式捕获有两种方式,分别是[=]和[&]。[=]表示以值捕获的方式捕获外部变量,[&]表示以引用捕获的方式捕获外部变量。可以让编译器根据函数体中的代码来推断需要捕获哪些变量。
那么为什么要避免使用隐式捕获呢?
如果一个由lambda创建的闭包的生命期超过了局部变量或者形参的生命期,那么闭包的引用将会空悬。
void addDivisorFilter()
{
auto calc1 = computeSomeValue1();
auto calc2 = computeSomeValue2();
auto divisor = computeDivisor(calc1, calc2);
filters.emplace_back(
[&](int value) { return value % divisor == 0; } // 危险!对divisor的引用会空悬
);
}
这代码有个定时炸弹。lambda引用了局部变量divisor
, 但是局部变量的生命期在addDivisorFilter
返回时终止,也就是在filters.emplace_back
返回之后。由于filters
容器不属于这个局部,我们采用的是引用的形式,当本体作为局部变量被销毁后,就会变为未定义行为。
即使我们加上显示引用捕获divisor
,会存在着同样的问题:
filters.emplace(
[&divisor](int value) // 危险!对divisor的引用依然会空悬
{ return value % divisor == 0; }
);
虽然依然存在问题,但我们可以从中看到lambda的活性依赖于divisor
的生命期,显示地写出能够提醒我们要确保divisor
的生命期至少和lambda闭包一样长。即使是我们知道一个closure只在当前作用域范围内使用(不同于上面的filters
),不会传播出去,使用隐式捕获依然可能出现一些问题。
template <typename C>
void workWithContainer(const C& container) {
auto calc1 = computeSomeValue1();
auto calc2 = computeSomeValue2();
auto divisor = computeDivisor(calc1, calc2);
using ContElemT = typename C::value_type;
using std::begin;
using std::end;
if (std::all_of(
begin(container), end(container),
[&](const ContElemT& value) { return value % divisor == 0; })) {
...
) else {
...
}
}
//c++14 auto can be used in params type
if (std::all_of(
begin(container), end(container),
[&](const auto& value) { return value % divisor == 0; }))
这段代码本身没什么问题,但你没办法保证不会有人把这段代码拷贝到其它地方,没注意这里有个默认的引用捕获,结果出现孤悬引用。
为了解决这个问题,人们提出了几种可能的解决方案:
(1)默认的值捕获 可行,但不能捕获指针
filters.emplace_back([=](int value)
{ return value % divisor == 0; }
(2)拷贝一份成员变量,再捕获 可行
class Widget {
public:
...
void addFilter() const;
private:
int divisor;
void Widget::addFilter() const {
auto divisorCopy = divisor;
filters.emplace_back([divisorCopy] (int value)
{
return value % divisorCopy == 0;
});
}
};
(3)初始化捕获作用于14
void Widget::addFilter() const {
filters.emplace_back(
[divisor = divisor] (int value) {
return value % divisor == 0;
}
);
}
Item 32:Use init capture to move objects into closures.
有时候我们想把一个对象移动到closure中,比如一个只能移动的对象(std::unique_ptr
或std::future
),或是移动的代价远小于复制的对象(比如大多数的STL容器),这个时候默认的引用捕获和值捕获都无法做到。C++14提供了一种方式,叫“初始化捕获”,能满足这一需求。C++11无法直接实现,但后面会介绍一种间接实现的方式。
class Widget;
...
auto pw = std::make_unique<Widget>();
... // confiture *pw
auto func = [pw = std::move(pw)] { return pw->isValidated() && pw->isArchived(); };
pw = std::move(pw)
中,=
左边的是数据成员的名字,它的作用域就是这个closure;右边是它的初始化式,它的作用域就是closure所在的作用域。
举个更简单的例子:
auto testNumber = 10;
auto divisor = 3;
auto pw = make_unique<int>(divisor);
auto fun = [pw = std::move(pw)](auto& divided){ return divided / *pw; };
cout << fun(testNumber) << endl; // result is 3
C++11实现
先介绍一下std::bind
,上代码。
#include <iostream>
#include <functional>
using namespace std;
using namespace std::placeholders;
int main()
{
auto fun = [](int *array, int n, int num){
for (int i = 0; i < n; i++)
{
if (array[i] > num)
cout << array[i] << ends;
}
cout << endl;
};
int array[] = { 1, 3, 5, 7, 9 };
//_1,_2 是占位符
auto fun1 = bind(fun, _1, _2, 5);
//等价于调用fun(array, sizeof(array) / sizeof(*array), 5);
fun1(array, sizeof(array) / sizeof(*array));
cin.get();
return 0;
}
fun1
函数由闭包转化而来,bind就是用于这种捆绑转化,它通过占位符汲取了闭包的第一、二个参数,然后将第三个参数置为5。这样就形成了:
void fun(int* array,int n)
{
for (int i = 0; i < n; i++)
{
if (array[i] > 5)
cout << array[i] << ends;
}
cout << endl;
};
在C++11中我们可以用bind+lambda实现14中的移动捕获
std::vector<double> data;
...
auto func = std::bind(
[](const std::vector<double>& data) {...},
std::move(data)
);
默认情况下closure class的operator()
会被认为是const,因此我们在lambda中无法修改捕获的对象。这时我们可以给lambda添加上mutable
标识符,令它可以修改捕获的对象:
auto func = std::bind(
[](std::vector<double>& data) mutable {...},
std::move(data)
);
Item 33:Use decltype on auto&& parameters to std::forward them.
C++14的一项引入注目的新功能就是泛型lambda,即lambda的参数可以用auto
来修饰。
它的实现很直接:closure class的operator()
是个模板函数。给定下面的lambda:···
auto f = [](auto x) {return normalize(x);};
// closure class
class SomeClosureClass {
public:
template <typename T>
auto operator()(T x) const {
return normalize(x);
}
...
};
上面的例子中,如果normalize
处理左值参数和右值参数的方式上有区别,那么我们写的还不算对,应该用上完美转发。
auto f = [](auto&& x)
{ return normalize(std::forward<???>(x)); };
但问题是std::forward
的实例化类型是什么。通常的完美转发我们能有一个模板参数T
,但在泛型lambda中我们只有auto
。closure class的模板函数中有这个T
,但我们没办法用上它。用decltype
可以解决这个问题!
我们在Item3学到过:通过decltype
保证返回变量的本来类型这一特性,保证不丢失CV
限制符,和引用等。
auto f = [](auto&& x) {
return normalize(std::forward<decltype(x)>(x));
};
甚至还支持变量个数可变的泛型lambda:
auto f = [](auto&&... xs) {
return normalize(std::forward<decltype(xs)>(xs)...);
};