一. lambda的捕获方式
(一)3种捕获方式:
1. 按值捕获: [=]或[var],前者为按值的默认捕获方式
2. 按引用捕获:[&]或[&var],前者为按引用的默认捕获方式
3. 初始化捕获(C++14): 见后面的《广义捕获》及由其引申出来的移动捕获功能。这种捕获方式可以做到C++11中所有捕获方式能够做到的所有事情。
(二)默认捕获方式的陷阱 :[=]和[&]
1.按引用捕获会导致闭包(由lambda表达式创建的对象)中包含指向局部对象或形参的引用。一旦该闭包超过该局部变量或形参的生命期,那么闭包内的引用就会发生“引用悬空”。当然如果闭包和局部变量/形参的生命期相同,就不会出现这个问题。
2.按值的默认捕获极易受悬空指针影响(尤其是this),并且会让人产生lambda表达式是独立的、不受外界影响的错觉。
【编程实验】默认捕获方式的陷阱
#include <iostream> #include <vector> #include <memory> #include <functional> using namespace std; using FilterContainer = std::vector < std::function<bool(int)>>; //筛选函数的容器 FilterContainer filters; //1. 按引用捕获造成的“引用悬空”问题 void addDivisorFilter() { auto divisor = 10; //divisor可以是其它表达式通过运算的结果。 //将跟这个除数(divisor)相关的过滤函数添加到vector中。 //1.1 “引用悬空”问题 filters.emplace_back( [&](int value) {return value % divisor == 0;} //由于divisor是个局部变量,被按引用捕获。当addDivisorFilter //函数结束后,divisor被销毁。filters中相应的该闭包对象就存在 //一个绑定到被销毁变量的引用,形为“引用悬空”。由于这里是默 //认方式的按引用捕获,会捕获到所有的局部变量或形参,当lambda //表达式与局部变量的生命期不同时,由于捕获到变量众多,很容易 //一不小心使用到这些“悬空”的引用。 ); //1.2 解决方案 //filters.emplace_back( // [=](int value) {return value % divisor == 0; } //ok, 这里改成按值捕获,则lambda中的divisor是局部变量的副本。 //); } //2. 按值捕获this指针造成的“指针悬空”现象 class Widget { int divisor; public: Widget(int div = 5):divisor(div){} void addFilter() const { //2.1 可能存在空悬指针现象 filters.emplace_back( [=](int value) {return value % divisor == 0; } //注意这里的divisor是成员变量,不能被捕获。 //当按值捕获时,它是通过this指针来访问的。 //即divisor的生命期依赖于this所指对象本身。 ); //2.2 解决方案:复制divisor的副本到lambda中。 auto divisorCopy = divisor; filters.emplace_back( [divisorCopy](int value) {return value % divisorCopy == 0; } //捕获divisor的副本。 ); } }; void doSomeWork() { auto pw = std::make_unique<Widget>(5); //... //做些其它事情 pw->addFilter(); //当doSomeWork函数结束后,由于智能指针会自动释放widget对象。 //由于Widget对象的生命期比filters中相应的元素(lambda表达式)生命期短。 //因此,filters中就含有一个带有空悬指针的元素。 } //3. 按值捕获的表达式并不完全独立(lambda可能依赖外部的静态变量) void ByValDependentcy() { static auto divisor = 10; filters.emplace_back( [=](int value) {return value % divisor == 0; } //由于static无法被捕获,lambda是直接使用该 //变量的。 ); ++divisor; //意外修改了divisor。上述的lambda中的[=],由于按值捕获会给人造成lambda式是独立的错觉。 //实际上该lambda中是直接使用static变量的,其值会随着ByValDependentcy函数的调用而逐次 //递增。这与按值默认捕获所暗示的含义直接相矛盾。解决的方案:不要采用按值默认的捕获 //方案,取而代之的是采用广义捕获(见后面的《广义捕获》内容) } int main() { return 0; }
二. 初始化捕获(也称为广义lambda捕获)
(一)C++11中捕获机制的局限
1. lambda捕获的是局部变量或形参,不管是按值还是按引用捕获,这些都是具名的左值对象。而右值对象是匿名对象,无法被捕获。
2. 按值捕获时,左值是被复制到闭包中的。如果被捕获的对象是个只移动类型的对象时,因其无法被复制,就会出错。此外,如果被捕获的对象如果是一个占用内存较大的对象时,按值捕获显然效率很低。
(二)初始化捕获(C++14)
1. 格式:形如[mVar1 = localVar1, mVar2 = std::move(localVar2)](){};
2. 说明:
(1)mVar1和mVar2是闭包类的成员变量的名字。而位于“=”右侧的则是初始化表达式。
(2)mVar1和mVar2的作用域就是闭包类的作用域,即仅限于闭包类部可用。而“=”右侧的作用域则与该lambda表达式加以定义之处的作用域(即lambda式的父作用域)相同。
(3)初始化捕获使得可以在闭包类中指定成员变量的名字,以及使用表达式来初始化这些成员变量。
(三)利用初始化捕获来实现移动捕获功能
1. C++14中的移动捕获(通过初始化捕获将对象移入闭包)
2. C++11中通过类或std::bind模拟移动捕获(std::bind模拟的手法)
(1)将lambda表达式绑定到std::bind函数对象中,同时将需要捕获的对象移动到std::bind对象中。
(2)在lambda表达式中通过引用绑定到要“捕获”的对象上。
【编程实验】初始化捕获及移动对象
#include <iostream> #include <memory> #include <functional> #include <vector> using namespace std; //1. 通过初始化捕获将对象移动闭包中 class Widget { public: bool isValidated() const { return true; } //是否有效 bool isProcessed() const { return true; } bool isArchived() const { return true; } //是否存档 }; //2. 使用类来模拟移动对象 class IsValAndArch { using DataType = std::unique_ptr<Widget>; DataType mPW; public: //移动构造函数 explicit IsValAndArch(DataType&& ptr):mPW(std::move(ptr)) { } }; int main() { auto pw = std::make_unique<Widget>(); //创建Widget对象 //1. 通过初始化捕获将对象移动闭包中 //1.1 使用std::move将pw这个只移动对象移入闭包中(C++14) auto func1 = [mPW = std::move(pw)]{ return mPW->isValidated() && mPW->isArchived(); }; //1.2 可以在捕获列表中直接用表达式来初始化mPW成员变量。 auto func2 = [mPW = std::make_unique<Widget>()]{ return mPW->isValidated() && mPW->isArchived(); }; //2.使用类模拟将只移动对象移入仿函数中 auto func3 = IsValAndArch(std::make_unique<Widget>()); //仿函数对象,其中的unique是个只移动对象。 //3.通过std::bind模拟初始化捕获 std::vector<double> data; //3.1 使用初始化捕获,实现容易 auto func4 = [data = std::move(data)]{}; //将data对象移入闭包中。 //3.2 std::bind模拟移动对象到lambda中 //注意事项: //(1)bind对象(即func5)会将所有实参的副本保存其中。对于左值会实施复制构造,对于右值会实施移 // 动构造。因此,bind对象中,保留了第1个实参lambda和第2个实参data的副本,而第2个实参是通过 // std::move移动到bind对象中,成为其中的一个成员变量(模拟将data移入绑定对象) //(2)当调用func5时,该函数会将上述的data副本作为实参传递给其中的lambda表达式。 //(3)lambda表达式的形参data是个引用,它是个指向func5对象中的data的左值引用(注意不是右值,因 // 为在func5中副本本身是一个左值)。因此lambda对data的操作,都会实施在func5对象的data副本身上。 // 由于lambda的operator()是个const函数,因此其内的成员变量都带有const属性。但bind对象上的data // 副本并不带const修饰符。为了防止该data副本在lambda中被修改,将lambda的形参声明为常量引用。 //(4)由于bind对象存储所有实参的副本,因此bind对象中的lambda表达式也是一个副本,其生命期与bind对象一致。 auto func5 = std::bind( [](const std::vector<double>& data) {}, //第1个参数:lambda表达式, std::move(data) //第2个参数,将data移动到bind对象中 ); return 0; }