概述
Lambda简介
Lambda表达式最重要的特点就是能够极其方便地创建函数对象。
其实,Lambda表达式能做到的事情,手工都能做到,无非就是多打一些字。
但是,Lambda表达式提供的简洁、易用、功能之强大,真是香啊!
总的来说,Lambda表达式经常用于以下场景:
- 标准库STL中使用,如std::find_if, std::remove_if, std::count_if等
- 自定义比较函数的算法,如std::sort, std::nth_element, std::lower_bound等
- 能够为std::unique_ptr/std::shared_ptr快速创建自定义析构器
- 临时创建回调函数、接口适配函数供一次性调用
注意点:
- Lambda是表达式的一种,它是源码组成部分
- 闭包是Lambda表达式创建的运行期对象,根据捕获方式的不同,它持有数据的副本或引用
- 闭包类是实例化闭包的类。每个Lambda表达式都会触发编译器生成一个独一无二的闭包类,闭包中的语句为闭包类成员函数的可执行指令
- Lambda式常用于创建闭包并仅将其用作传递给函数的实参
- 闭包可以复制,对应于一个Lambda表达式的闭包类型可以有多个闭包,如下:
int x;
auto c1 = [x](int y){ return x * y > 34; };
auto c2 = c1; // c2是c1的副本
auto c3 = c2; // c3是c2的副本
// c1,c2,c3都是同一Lambda式产生的闭包的副本
本文不再介绍基本使用,而关注使用经验,如默认捕获可能带来的问题、有哪些好的实践及与std::bind的对比等,关于Lambda表达式的基础知识,请参考 c++中的Lambda表达式
避免默认捕获
先说结论,按引用或按值的默认捕获都可以导致使用空悬的引用,导致程序未定义的行为!
- 按引用默认捕获
按引用捕获会导致闭包包含对局部变量或定义Lambda式的作用域内(即定义Lambda式的那个函数)的形参的引用。
一旦闭包越过了该局部变量或形参的作用域,它们就会析构,闭包内对它们的引用就会空悬。
举例来说,如下代码:
// 一个添加除数筛选器的函数
void addDivisorFilter()
{
auto divisor = calcDivisor(); // 通过某种方式计算得到除数
// 把函数对象添加到vector中,vecotr的原型为:std::vector<std::function<bool(int)>>
filters.emplace_back(
[&](int value) {return value % divisor == 0;} // 局部变量按引用默认捕获
);
}
注意,默认捕获了对局部变量divisor的引用,当函数走到右大括号时,局部变量析构,而筛选器对函数对象还在傻傻地运行着,但引用已经空悬!
其实,把引用默认捕获修改为 &divisor
这样显式捕获时,依然还会存在那个问题。但这样大概能够直接看到这个局部变量在函数返回时就拜拜了,更容易查找问题。
那要怎么办呢,其实也简单,针对本例而言,把按引用的捕获修改为按值的捕获就行了。传入了闭包局部变量的副本,局部变量析构时对它不会有影响。
- 按值的默认捕获
但按值捕获也非万能之策。
考虑如下代码:
class Foo
{
public:
//...
void addFilter() const;
private:
int divisor;
};
void Foo::addFilter() const
{
filters.emplace_back(
[=](int value) { return value % divisor == 0; } // 按值的默认捕获,能通过编译,但是去掉=号或者直接写divisor捕获,则编译错误
);
}
如果有这个疑问:divisor并不是在创建Lambda式的作用域内可见的非静态局部变量或形参,它是怎么通过编译的?就要了解一下this裸指针的隐式应用了。
类内的每一个非静态成员函数都持有一个this,每当使用类的成员变量时,实际上使用的是 this->localVarName
。
所以在成员函数内按值默认捕获,就捕获了this。使用的也是 this->divisor
,而不是单个变量。那么,既然是这样,this的生命期就重要了,它可不能比闭包短哦。
但下面的代码就没有那么幸运了:
void doSomework()
{
auto pf = std::make_unique<Foo>(); // 使用智能指针,以自动管理资源,注意,需要c++14
pf->addFilter(); // 使用addFilter函数,添加筛选器函数对象,它会捕获this
}
如注释所言,但是当该函数结束时,pf也就要销毁了,this将不复存在,啊,filters中就含有了空悬指针的元素。只能加班调试了。
怎么解决这个问题呢?
简单的方法是,在addFilter函数中先创建一个divisor的局部变量,再按值捕获即可。这规避了捕获this的问题。
c++14中有更好更直接的方法,就是广义Lambda捕获。如下:
// c++14
void Foo::addFilter() const
{
filters.emplace_back(
[divisor=divisor](int value) { return value % divisor == 0; } // 广义Lambda捕获,把等号右侧的变量赋值给左侧变量,左侧变量作为闭包的参数,不会空悬
);
}
完美!
注意,虽然Lambda表达式只能捕获创建Lambda式的作用域内可见的非静态局部变量或形参,但Lambda式内依然可以使用静态存储期对象。按值捕获可能会产生误解。
如下代码改自上述示例:
// 一个添加除数筛选器的函数
void addDivisorFilter()
{
static auto divisor = calcDivisor(); // 现在为static
// 把函数对象添加到vector中,vecotr的原型为:std::vector<std::function<bool(int)>>
filters.emplace_back(
[=](int value) {return value % divisor == 0;} // 局部变量按值默认捕获,实际上未捕获任何变量,因为divisor是static的
);
++divisor;
}
你以为divisor是按值捕获,所以闭包内是它的副本,它永远正确不会改变。
但你以为的是你以为的,不是我以为的。你只是使用了static的divisor,按值的默认捕获未捕获任何东西!所以注意到最后一行的自增操作,你的程序每运行一次,divisor都会不一样哦!哈哈,又要加班调试了。
所以,尽量不要使用默认捕获模式。
c++14中的初始化捕获
c++14中不再支持默认捕获,而是改为初始化捕获(即前文使用过的广义Lambda捕获)。使用初始化捕获,有以下特点:
- 指定由Lambda式生成的闭包类中成员变量的名字
- 指定用以初始化成员变量的表达式
- 支持移动对象
示例如下:
class Foo
{
public:
bool isValidated() const;
}
auto func = [pf = std::make_unique_ptr<Foo>()] {return pf->isValidated();}; // 初始化捕获支持表达式
其中,捕获语句中:
- 等号左侧为闭包类的成员变量的名字,作用域为闭包类的作用域
- 等号右侧为初始化表达式,作用域与Lambda表达式定义处的作用域相同
与使用c11的Lambda式经常出现的问题相比,c14无疑是更好的选择。
优先选用Lambda式而非std::bind
两者对比:
- Lambda式可读性高,编写简单,代码量少,bind会出现类似_1占位符(占位符难以理解,其具体类型需要查看原函数声明才能得知)
- 对于需要调用重载函数时,Lambda式不会有歧义,当重载函数更改后能自动适配,bind需要通过强制类型转换到函数指针再使用
- 由于Lambda式可以被编译器内联,而函数指针不可以,所以使用Lambda有可能生成运行效率更高的代码
- bind对于实参是按值传递的事实未明显标示,对形参使用引用传递也是如此,使用Lambda式都是显式标识的
使用bind的场合:
- 移动捕获。c11中未提供移动捕获特性,只能通过结合bind来模拟。
- 多态函数对象。因为绑定对象的函数调用运算符利用了完美转发,可以接受任意类型的实参。如:
class PolyFoo
{
public:
template<typename T>
void operator()(const T& param);
}
PolyFoo pf;
auto boundPF = std::bind(pf, _1); // 可以通过任意类型的实参调用
// boundPf(200);
// boundPf(nullptr);
// boundPf("abc");
其实,上述两种使用场合在c14中已经不复存在:
- c14默认直接支持移动,直接使用初始化捕获std::move即可
- 使用auto类型形参的Lambda式,如下:
auto boundPF = [pf](const auto& param) { pf(param); };
总之,在现代化的代码中,尽量Lambda表达式会带来很多好处。
总结
Lambda表达式使用中,会有一些陷阱,需要注意,不要使用默认的捕获。
c14中,使用初始化捕获,不再支持默认捕获,这是一个非常有益的变动,推荐使用。
在c14中,Lambda表达式基本可以取代难懂的bind,并更有可能获得高效的执行代码。
参考资料
《Effective Modern C++》
最后
以上就是无私热狗为你收集整理的深入理解c++中的Lambda表达式的全部内容,希望文章能够帮你解决深入理解c++中的Lambda表达式所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复