lambda表达式是什么
参考?:c++函数对象与lambda表达式实现原理_~怎么回事啊~的博客-CSDN博客
lambda避免默认捕获模式
引用传递
#include <iostream>
int var_global = 9;
class Bar{
public:
Bar(int val){
var_dataMember = val;
}
auto func(int var_localParam){
static int var_localStatic = 10;
int var_local = 12;
return [&](){
std::cout <<" var_localParam : " << var_localParam << ";var_local :"
<< var_local << std::endl;
std::cout <<" var_localStatic : " << var_localStatic << ";var_dataMember :"
<< var_dataMember << ";var_global : " << var_global << std::endl;
};
}
private:
int var_dataMember;
};
int main(){
Bar bar(8);
auto f = bar.func(6);
f();
return 0;
}
将上述的代码复制到:C++ Insights
主要用于看代码编译展开后的具体情况,点击run
#include <iostream>
int var_global = 9;
class Bar
{
public:
inline Bar(int val)
{
this->var_dataMember = val;
}
inline __lambda_16_16 func(int var_localParam)
{
static int var_localStatic = 10;
int var_local = 12;
class __lambda_16_16
{
public:
inline /*constexpr */ void operator()() const
{
std::operator<<(std::operator<<(std::cout, " var_localParam : ").operator<<(var_localParam), ";var_local :").operator<<(var_local).operator<<(std::endl);
std::operator<<(std::operator<<(std::operator<<(std::cout, " var_localStatic : ").operator<<(var_localStatic), ";var_dataMember :").operator<<(__this->var_dataMember), ";var_global : ").operator<<(var_global).operator<<(std::endl);
}
private:
int & var_localParam;
int & var_local;
Bar * __this;
public:
__lambda_16_16(Bar * _this, int & _var_localParam, int & _var_local)
: __this{_this}
, var_localParam{_var_localParam}
, var_local{_var_local}
{}
} __lambda_16_16{this, var_localParam, var_local};
return __lambda_16_16;
}
private:
int var_dataMember;
public:
};
int main()
{
Bar bar = Bar(8);
__lambda_16_16 f = bar.func(6);
f.operator()();
return 0;
}
lambda表达式本质上是一个函数对象,在这里是__lambda_16_16:
上述的代码在gcc (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0下运行结果:
ok@ok-Precision-3630-Tower:/data/test/c++$ ./testlambda
var_localParam : 32765;var_local :12
var_localStatic : 10;var_dataMember :8;var_global : 9
由于int var_localParam形参在函数结束后被释放,var_localParam被引用捕捉,因此是垃圾值,函数栈上分配的var_local 由于编译器原因,还可以访问?
值传递捕获的是this指针
#include <iostream>
class Bar{
public:
Bar(int val){
divisor_ = val;
}
auto bindTest() {
return [=] (int value)-> int {
std::cout << "divisor_ :" << divisor_<< std::endl;
return value - divisor_;
};
}
private:
int divisor_;
};
auto getF() {
Bar bar(8);
auto f = bar.bindTest();
return f;
}
int main(){
auto f = getF();
std::cout << f(10) << std::endl;
return 0;
}
输出:
ok@ok-Precision-3630-Tower:/data/test/c++$ ./testlambda
divisor_ :21897
-21887
在getF 调用结束后,Bar bar 类的实例化对象被释放,divisor_保存的数据成为垃圾值
解决this释放的问题:增加一个局部变量,拷贝成员变量的值并且值传递到lambda中:
C++14 初始化捕获
无法在Lambda中捕获静态变量
具有静态存储持续时间的变量不需要捕获,因此无法捕获。您可以在lambda中简单地使用它
#include <iostream>
auto bindF(){
static int divisor = 10;
divisor++;
std::cout <<"divisor:" << divisor <<";&divisor:" << &divisor << std::endl;
return [=](int value) -> int {
std::cout <<"lambda divisor:" << divisor <<";&divisor:" << &divisor << std::endl;
return value - divisor;
};
}
int main(){
auto f1 = bindF();
std::cout << f1(10) << std::endl;
auto f2 = bindF();
std::cout << f2(10) << std::endl;
return 0;
}
输出:
ok@ok-Precision-3630-Tower:/data/test/c++$ ./testlambda
divisor:11;&divisor:0x56072a4ba010
lambda divisor:11;&divisor:0x56072a4ba010
-1
divisor:12;&divisor:0x56072a4ba010
lambda divisor:12;&divisor:0x56072a4ba010
-2
C++14的Lambda新特性:对捕获变量的初始化
在C++14 中新增了对捕获变量的初始化,且不影响原来的变量
int a;
auto func = [a = 10]()mutable{
cout << a << endl;
};
func();
cout << "a:" << a << endl; //这里的a还是没有初始化的状态
如果你有一个对象,其复制操作开销昂贵,而移动操作成本低廉,而你又需要把该对象放入闭包,那么你肯定更愿意移动该对象,而非复制它。
C++14为我们提供了一个全新的捕获方式---初始化捕获,也叫做广义lambda捕获,使用初始化捕获,则你会得到机会指定:
由lambda生成的闭包类中的成员变量名字。
一个表达式,用以初始化该成员变量。
例如使用初始化捕获将std::unqiue_ptr移动到闭包内:
class Widget { //一些有用的型别
public:
...
bool isValidted() const;
bool isProcessed() const;
bool isArchived() const;
private:
...
};
auto pw = std::make_unique<Widget>(); //创建Widget
//关于std::make_unique,参见Item 21
... //配置*pw
auto func = [pw = std::move(pw)] //采用std::move(pw)
{ return pw->isValidated() && //初始化闭包类的数据成员
pw->isArchived();};
[] 中的那段代码就是初始化捕获,位于“=”左边的,是你所指定的闭包类成员变量的名字,而位于其右侧是初始化表达式,且它们处于不同的作用域,左侧作用域就是闭包类的作用域,右侧的作用域与lambda式加以定义之处的作用域相同。
pw=std::move(pw)表达了"在闭包中创建一个成员变量pw,然后使用针对局部变量pw实施std::move的结果来初始化该成员变量"。
C++11显然并不支持这种初始化捕获,那该如何在C++11中实现移动捕获呢?
在C++11中按移动捕获可以采用以下方法模拟,只需要:
把需要捕获的对象移动到std::bind产生的函数对象中。
给到lambda式一个指涉到欲“捕获”的对象的引用。
例如,如果你想创建一个局部的std::vector对象,向其放入一组值,然后将其移动入闭包。在C++14使用初始化捕获,非常简单:
std::vector<double> data; //欲移入闭包的对象
... //灌入数据
auto func = [data = std::move(data)] //C++14的初始化捕获
{/* 对数据加以运用 */}
但是在C++11中,你必须借助std::bind生成函数对象:
std::vector<double> data; //同前
... //同前
auto func = std::bind(
[](const std::vector<double>& data) //C++11中模拟移动捕获的部分
{/* 对数据加以运用 */},
std::move(data)
);
和lambda表达式类似地,std::bind也生成函数对象,第一个实参是个可调用对象,第二个实参表示传给该对象的值。
我们知道,std::bind对于每个左值实参,在绑定对象内的对应的对象内对其实施的是复制构造,而对每个右值实参,实施的则是移动构造。所以当一个绑定对象被“调用”,也就是当func(绑定对象)被调用时,func内经由移动构造出所得到的data的副本就会作为实参传递给那个原先传递给std::bind的lambda式。
至此,我们应该知道以下知识点:
移动构造一个对象入C++闭包是不可能实现的,但移动构造一个对象入绑定对象则是可以实现的
欲在C++11中模拟移动捕获,需要包含以下步骤:
- 先移动构造一个对象入绑定对象
- 然后按照引用把移动构造所得的对象传递给lambda表达式
因为绑定对象的生命周期和闭包相同,所以针对绑定对象中的对象和闭包内的对象可以采用相同手法处置
lambda表达式实现完美转发,对auto&&型别的形参使用decltype,以std::forward之
泛型lambda式(generic lambda)是C++14最振奋人心的特征之一,lambda可以在形参规格中使用auto。这个特性的实现十分直接了当:闭包类中的operator()采用模板实现。例如,给定下述lambda式:
auto f = [](auto x) { return func(normalize(x));};
以上语句翻译的闭包类代码如下:
class SomeComilerGeneratedClassName {
public:
template<typename T> //auto型别的返回值
auto operator()(T x) const //参见Item 3
{ return func(normalize(x));}
...
}; //闭包类的其他功能
在本例中,lambda式对x实施的唯一动作就是将其转发给normalize。但是如果normalize区别对待左值和右值,那么该lambda式撰写的是有问题的,因为在此lambda总会传递左值(形参x)给normalize,即使传递给lambda式的实参是个右值。
该lambda式的正确撰写方式是把x完美转发给normalize,这就要求代码中修改两处,首先,x要改成万能引用(参见Item 24);其次,使用std::forward(参见Item 25)把x转发给normalize。概念上不难理解,这两处的修改都是举手之劳:
auto f = [](auto&& x){ return func(normalize(std::forward<???>(x)));};
可是问题就出在,上面代码里面的???应该怎么办呢?
通常情况下,在使用完美转发的时候,你是在一个接受型别形参T的模板函数中,所以,你写std::forward<T>就好。但是在泛型lambda式中,却没有可用的型别形参T。在lambda式生成的闭包内的模板化operator()函数中的确有个T,但是在lambda式中无法指涉,所以也没用。
前面我们已经说过,如果把实参左值传递给万能引用型别的形参,则该形参的型别会被推导为左值引用,如果传递是右值,则该形参会成为右值引用。那这也就意味着我们可以在该lambda表达式中探查x的型别,判断传入的实参是左值还是右值。而decltype则可以完成探查工作,如果传入的是个左值,decltype(x)将会产生左值引用型别,如果传入的是个右值,decltype(x)将会产生右值引用。
我们还知道,使用std::forward时惯例是:用型别形参为左值引用表明想要返回左值,而用非引用型别时来表达想要返回的右值。
再看看我们的lambda式,如果x绑定了左值,decltype(x)将产生左值引用型别。这符合惯例。不过,如果x绑定的是个右值,decltype(x)将会产生右值引用惯例,并非惯例的右值。
但是这里需要说明的是,在使用std::forward时,使用一个右值引用型别和使用一个非引用型别,会产生相同结果。所以如果x绑定的是个右值,decltype(x)后的std::forward()依然符合惯例。
示例代码如下:
//单一变量
auto f =
[](auto&& param)
{
return func(normalizer(std::forward<decltype(param)>(param)));
}
//多变量
auto f =
[](auto&&... param)
{
return func(normalizer(std::forward<decltype(param)>(param)...));
}
优先选用lambda表达式,而非std::bind
优势1:可读性更好
我们假设有个函数用来设置声音警报,示例代码:
// 表示时刻的型别typedef(语法参见Item 9)
using Time = std::chrono::steady_clock::time_point;
// 关于“enum class”,参见Item 10
enum class Sound {Beep, Siren, Whistle};
// 表示时长的型别typedef
using Duration = std::chrono::steady_clock::duration;
// 在时刻t,发出声音s,持续时长d
void setAlarm(Time t, Sound s, Duration d);
这里进一步假设,在程序的某处,我们想要设置在一小时之后,发出警报并持续30秒。警报的具体声音,却尚未确定。
这么一来,我们可以撰写一个lambda式,修改setAlarm的接口,这个新的接口只需要指定声音即可:
// setSoundL("L"表示Lambda)是个函数对象
// 它接受指定一个声音
// 该声音将在设定后一小时发出,并持续30秒
auto setSoundL =
[](Sound s)
{
//使std::chrono组件不加限定饰词即可使用
using namespace std::chrono;
setAlarm(steady_clock::now() + hours(1), //报警发出的时刻为1小时后
s,
seconds(30)); //持续30秒
};
如果是在C++14里,那么上述的代码可以写的更具可读性:
auto setSoundL =
[](Sound s)
{
using namespace std::chrono;
using namespace std::literals; //C++14支持实现后缀
setAlarm(steady_clock::now() + 1h, //这里直接用1h表示
s,
30s); //30s表示
};
那么如果用std::bind来写会是什么样呢?下面的代码其实包含了一个错误,我们后续会修复它,先看看代码:
using namespace std::chrono;
using namespace std::literals;
using namespace std::placeholders; //这里是因为要用bind对应的占位符
auto setSoundB = //B表示bind
std::bind(setAlarm,
steady_clock::now() + 1h, //这里有个错误!
_1,
30s);
对于初学者而言,这种“_1"占位符简直好比天书,但即使是行家,也许脑补出从阿占位符中数字到它在std::bind形参列表位置的映射关系,才能理解,在调用setSoundB时传入的第一个参数,会作为第二个实参传递给setAlarm。而且你还不知道这个实参的类型是什么,需要查看setAlarm的声明。
接下来看看错误在哪里:
在lambda式中,表达式steady_clock::now() + 1h是setAlarm的实参之一,这一点清清楚楚,该表达式会在setAlarm被调用的时刻评估求值。这样做合情合理,我们就是想要在setAlarm被调用后的一个小时之后启动报警。
在std::bind的调用中,steady_clock::now() + 1h作为实参被传递给了std::bind,而非setAlarm。意味着表达式评估求值的时刻是在调用std::bind的时刻,并且求得的时间结果会被存储在结果绑定对象中。最终导致的结果是,报警被设定的启动时刻是在调用std::bind的时刻之后的一个小时,而并非调用setAlarm的时刻之后的一个小时!
想解决这个问题,就像需要让std::bind来延迟表达式的评估求值到调用setAlarm的时候,而实现这一点的途径是在原来的std::bind上再嵌套一个std::bind.
//C++14,标准运算符模板的模板型别实参大多数情况可以不写
auto setSoundB =
std::bind(setAlarm,
std::bind(std::plus<>(), steady_clock::now(), 1h),
_1,
30s);
//C++11
auto setSoundB =
std::bind(setAlarm,
std::bind(std::plus<steady_clock::time_point>(),
steady_clock::now(),
hours(1)),
_1,
seconds(30));
事情到这里,已经很明显的显示出lambda的优势了,可读性强了不少。
优势2:遇到重载也没事
一旦对setAlarm实施了重载,新的问题就会马上出现。
假如有个重载版本会接受第四个形参,用以指定报警的音量:
enum class Volume {Normal, Loud, LoudPlusPlus};
void setAlarm(Time t, Sound s, Duration d, Volume v);
之前的lambda表示没问题,可以正常调用3形参版本重载。
但是对std::bind的调用,就无法通过编译了:
auto setSoundB =
std::bind(setAlarm, //错误!这里不知道如何选择了
std::bind(std::plus<steady_clock::time_point>(),
steady_clock::now(),
hours(1)),
_1,
seconds(30));
错误的根因在于编译器只拿到一个函数名,但是这个函数本身是多义的。
如果还是要让std::bind能运作,那么要写成这样:
using SetALarm3ParamType = void(*)(Time t, Sound s, Duration d);
auto setSoundB =
std::bind(static_cast<SetALarm3ParamType>(setAlarm),
std::bind(std::plus<steady_clock::time_point>(),
steady_clock::now(),
hours(1)),
_1,
seconds(30));
优势3:可能性更高的生成更快的代码
但即便你觉得这样写好,多写几行没什么问题,但这么写还是带来了更大的问题:
在SetSoundL的函数中,调用setAlarm采用的是常规的函数唤起方式,这么一来,编译器就可以用惯常的手法将其内联:
setSoundL(Sound::Siren); //这里,setAlarm的函数体大可以被内联
可是,std::bind的调用传递了一个指涉到setAlarm的函数指针,而那就意味着setAlarm的调用是通过函数指针发生的。编译器不太会内联掉通过函数指针发起的函数调用,所以后一种写法被内联的几率大大降低。所以lambda式形式的调用被优化的可能性更高。
而不仅如此,上面的例子仅仅涉及一个函数调用,如果你想做的事情比这更复杂,使用lambda式的好处会急剧增大。
看下面一个例子,需求是判断某个参数是否在最大值和最小值之间:
//C++14
auto betweenL =
[lowVal, highVal]
(const auto& val)
{ return lowVal <= val && val <= highVal;};
//std::bind C++14
using namespace std::placeholders;
auto betweenB =
std::bind(std::logical_and<>(),
std::bind(std::less_equal<>(), lowVal, _1),
std::bind(std::less_equal<>(), _1, highVal));
C++11还不支持模板泛型自动推导,还要改成这样:
//std::bind C++11
using namespace std::placeholders;
auto betweenB =
std::bind(std::logical_and<bool>(),
std::bind(std::less_equal<int>(), lowVal, _1),
std::bind(std::less_equal<int>(), _1, highVal));
这么一对比,已经太明显不过了。
优势4:参数类型一目了然
试想这样一个场景,需要压缩一个类,然后返回这个类的副本:
enum class CompLevel { Low, Normal, High};
Widget compress(const Widget& w, CompLeve leve)
然后写了个函数对象包装一下:
Widget w;
using namespace std::placeholders;
auto compressRateB = std::bind(compress, w, _1);
但是这里w是按值传递的还是按引用传递的,就让人很迷惑了。(这里有个前提,std::bind默认就是按值传递的,如果要用按引用,要显示写成:
auto compressRateB = std::bind(compress, std::ref(w), _1);
而lambda就很明显是按值:
auto compressRateL =
[w](CompLeve lev)
{ return compress(w, lev);};
不仅仅是这里声明和定义的地方让人迷惑,调用的形式也让人不清不楚:
//Lambda式
compresssRateL(CompLevel::High); //实参按值传递
//bind式
compresssRateB(CompLevel::High); //这里是按照什么呢?
答案又会让你出乎意料,而且还是死记硬背没有原因的:
结论
C++14里已经可以忘记std::bind了。
C++11里还有两种受限场合可以使用:
移动捕获:C++11语言不支持初始化捕获,只能用bind来模拟
多态函数对象: 因为绑定对象的函数调用运算符利用了完美转发,它就可以接受任何型别的实参。这个特点再你想要绑定的对象具有一个函数调用运算符模板是,是有利用价值的。
例如:
class PolyWidget {
public:
template<typename T>
void operator()(const T& param);
...
};
这里来个bind:
PolyWidget pw;
auto boundPw = std::bind(pw, _1);
这样一来,boundPw就可以通过任意性别的实参加以调用:
boundPw(1995);
boundPw(nullptr);
boundPw("Adam Xiao");
在C++11中的lambda式是办不到的,因为不支持泛型,但是C++14里依旧可以做到
//C++14
auto boundPw = [pw](const auto& param)