文章目录

  • ​​lambda表达式使用和避坑​​
  • ​​0 准备​​
  • ​​1 使用​​
  • ​​1.1 初始化捕获/广义捕获(generalized lambda capture)​​
  • ​​1.1.1 `std::bind`补充​​
  • ​​1.2 对`auto&&`类型的形参使用`decltype`并使用`std::forward`传递​​
  • ​​2 注意事项​​
  • ​​2.1 避免默认捕获方式​​

lambda表达式使用和避坑

0 准备

基本概念:

  • lambda表达式:表达式的一种;
  • 闭包是lambda式创建的运行期对象,根据不同的捕获模式,闭包会持有数据的副本或引用;
  • 闭包类是实例化闭包的类,每个闭包编译器都会生成独一无二的闭包类,而闭包中的语句会变成闭包类成员函数的可执行指令。

基础的使用请看​​博文1​​,​​博文2​​,本文主要讲解高级使用以及避坑。

lambda表达式的格式:

[capture list] (params list) mutable exception-> return type { function body }

lambda式的原理就是生成一个闭包类,lambda式的参数和内容就是重载函数调用操作符函数中的参数和内容。示例代码如下:

int main(){
int num = 5;
auto lambdaL = [num](int val){
return val == num;
};
bool b1 = lambdaL(5);
}

等价于下面的代码:

class ClosureClass{
private:
int localVal;
public:
ClosureClass(int num):localVal(num){}
auto operator()(int val) const{
return val == localVal;
}
};

int main(){
int num = 5;
ClosureClass lambdaL2(num);
bool b2 = lambdaL2(5);
}

1 使用

1.1 初始化捕获/广义捕获(generalized lambda capture)

初始化捕获的好处,可以指定:

  • 由lambda生成的闭包类中的成员变量名字;
  • 一个表达式,用以初始化该变量。

实现建议:

  • C++11中,经由手工实现的类或​​std::bind​​去模拟初始化捕获。
  • C++14中使用初始化捕获将对象移入闭包;

C++14中的实现:

class TestWidget{
public:
bool isValidated()const {return true;}
bool isProcessed() const {return true;}
bool isArchived() const {return true;}
};

int main(){
// auto pw = std::make_unique<TestWidget>();
// //pw配置修改
//...
// auto func =[pw = std::move(pw)]{//初始化捕获
// return pw->isArchived() && pw->isValidated();
// };
//如果不对pw进行配置
auto func = [pw = std::make_unique<TestWidget>()]{
return pw->isArchived() && pw->isValidated();
};
}

由于C++11中没有办法将只移对象【move-only object,例如​​std::forward​​​、​​std::unique_ptr​​】放入闭包,这样就没有办法用低廉的移动操作来代替复制操作。

C++11实现上面案例的代码为:

class TestWidget{
public:
bool isValidated()const {return true;}
bool isProcessed() const {return true;}
bool isArchived() const {return true;}
};

class IsValiAndArch{
public:
using DataType = std::unique_ptr<TestWidget>;
explicit IsValiAndArch(DataType&& ptr):pw(std::move(ptr)){}
bool operator()() const{return pw->isValidated() && pw->isArchived();}
private:
DataType pw;
};

int main(){
auto func = IsValiAndArch(std::make_unique<TestWidget>());
}

如果想用lambda模拟上面的移动捕获,可以按下面的方法:

  • 1,把捕获的对象移动到​​std::bind​​产生的函数对象中;
  • 2,给lambda式一个指涉欲捕获对象的引用.
auto func = std::bind([](const std::unique_ptr<TestWidget> pw){return pw->isValidated() && pw->isArchived();},
std::make_unique<TestWidget>());

上面变量加​​const​​​的原因,是因为默认lambda生成闭包的​​operate()​​​函数都会带有​​const​​​,所以闭包内的所有成员变量在lambda式中都有带有​​const​​​修饰词。如果lambda式声明就带有​​mutable​​​,生成闭包的​​operate()​​​函数就不会带有​​const​​。

1.1.1 ​​std::bind​​补充

​std::bind​​函数说明:

  • ​std::bind​​的第一个参数是可调用对象,接下来的所有实参表示传递给改对象的值;
  • ​std::bind​​返回的函数对象为绑定对象(bind object);
  • 绑定对象使用的都是实参的副本(按值存储),如果想对实参进行按引用存储的方法,可以对实参实施​​std::Ref()​​;
  • 由于绑定对象的所有实参都是按引用传递,因此绑定对象内,左值实参,实施复制构造,右值实参,实施移动构造;

下面的例子作为补充示范例子:

//实现移动捕获

std::vector<double> data;
int main(){
//C++14
auto func = [data = std::move(data)]{
//data操作
};
//C++11
auto func2 = std::bind([](const std::vector<double>& data){
//对data操作
}, std::move(data));
}

//实现绑定到具备模版化的函数调用运算符的对象

class PolyWidget{
public:
template<typename T>
void operator()(const T& param) const{}
};


int main(){
PolyWidget pw;
auto boudPW = [pw](const auto& param){pw(param);};
boudPW(1930);
boudPW(nullptr);
boudPW("hahahah");

PolyWidget pw2;
auto boudPW2 = std::bind(pw2, std::placeholders::_1);
boudPW2(1930);
boudPW2(nullptr);
boudPW2("hahahah");
}

​std::bind​​的使用建议:

  • 仅在C++11中使用​​std::bind​​来实现移动捕获,或是绑定到具备模版化的函数调用运算符的对象。
  • lambda相较于​​std::bind​​而言,具有可读性好、表达力强、可运行效率高。

下面的例子,将用来说明这些优点:

被调用的函数,作用是在设置时间点t发出警报s并持续d时长:

//表示时刻的类型
using Time = std::chrono::steady_clock::time_point;
//声音类型
enum class Sound{Beep, Siren, Whistle};
//表示时长的类型
using Duration = std::chrono::steady_clock::duration;

//设置警报
void setAlarm(Time t, Sound s, Duration d){
}
  • 可读性:

在C++14条件下,使用lambda表达式实现一个小时后发出警报并持续30s:

int main(){
//调用版本1(C++11)
auto setSoundL = [](Sound s){
using namespace std::chrono;
setAlarm(steady_clock::now() + hours(1), s, seconds(30));
};
setSoundL(Sound::Beep);
//调用版本2(C++14)
//简化后的调用
auto setSoundL2 = [](Sound s){
using namespace std::chrono;
using namespace std::literals;//汇入C++14实现的后缀
setAlarm(steady_clock::now() + 1h, s, 30s);
};
setSoundL2(Sound::Siren);
}

在C++14条件下,使用​​std::bind​​​实现同样的功能,但是下面的代码还存在着问题,因为​​steady_clock::now()​​​作为实参传递给绑定对象,所以警报启动时刻实在调用​​std::bind​​​后的一个小时,而非调用​​setAlarm​​​后的一个小时。【代码中的​​_1​​​表示给​​std::bind​​​传参数的占位符,即传递给​​std::bind​​​的第一个参数,会作为第二个参数传入到​​setAlarm​​函数中】

int main(){
using namespace std::literals;
using namespace std::chrono;
using namespace std::placeholders;
auto setSoundB = std::bind(setAlarm, steady_clock::now() + 1h, _1, 30s);
setSoundB(Sound::Siren);
}

使用​​std::bind​​改写后到版本(C++14版本):

using namespace std::literals;
using namespace std::chrono;
using namespace std::placeholders;
auto setSoundB2 =
std::bind(setAlarm,
std::bind(std::plus<>(), steady_clock::now(), 1h),
_1,
30s);
setSoundB2(Sound::Siren);

使用​​std::bind​​改写后到版本(C++11版本)【目前有错】:

auto setSoundB3 =
std::bind(setAlarm,
std::bind(std::plus<steady_clock::time_point>(),
steady_clock::now(),
hours(1)),
_1,
seconds(30));
setSoundB3(Sound::Siren);

另一个例子,求实参是否在极小值和极大值之间:

lambda版本:

int lowVal = 1;
int highVal = 100;
//lambda
//c++14版本
auto betweenL = [lowVal, highVal](const auto & val){
return lowVal <= val && highVal >= val;
};
//C++11版本
auto betweenL2 = [lowVal, highVal](int val){
return lowVal <= val && highVal >= val;
};

std::cout<<betweenL(20)<<std::endl;
std::cout<<betweenL2(20)<<std::endl;

​std::bind​​版本如下,明显就复杂很多:

int lowVal = 1;
int highVal = 100;
//c++14
auto betweenB = std::bind(std::logical_and<>(),
std::bind(std::less_equal<>(), lowVal, std::placeholders::_1),
std::bind(std::less_equal<>(), std::placeholders::_1 ,highVal));
std::cout<<betweenB(20)<<std::endl;
//c++11
auto betweenB2 = std::bind(std::logical_and<bool>(),
std::bind(std::less_equal<int>(), lowVal, std::placeholders::_1),
std::bind(std::less_equal<int>(), std::placeholders::_1 ,highVal));
std::cout<<betweenB2(20)<<std::endl;
  • 表达力强(如遇到重载函数)

被调用的函数:

//表示时刻的类型
using Time = std::chrono::steady_clock::time_point;
//声音类型
enum class Sound{Beep, Siren, Whistle};
//音量
enum class Volume { Normal, Loud, LoudPlusPlus };
//表示时长的类型
using Duration = std::chrono::steady_clock::duration;

void setAlarm(Time t, Sound s, Duration d){}
//函数重载
void setAlarm(Time t, Sound s, Duration d, Volume v){}

lambda式很容易解决,lambda版本的,正常运行。

auto setSoundL2 = [](Sound s){
using namespace std::chrono;
using namespace std::literals;//汇入C++14实现的后缀
setAlarm(steady_clock::now() + 1h, s, 30s);
};
setSoundL2(Sound::Siren);

但是​​std::bind​​​版本的代码,却因为不知道选择哪个版本的​​setAlarm​​​出现编译错误(​​error: no matching function for call to 'bind'​​)。解决方法就是使用强制类型转换。

using namespace std::literals;
using namespace std::chrono;
using namespace std::placeholders;
using SetAlarm3ParamType = void(*)(Time t, Sound s, Duration d);
auto setSoundB2 =
std::bind(static_cast<SetAlarm3ParamType>(setAlarm),
std::bind(std::plus<>(), steady_clock::now(), 1h),
_1,
30s);
setSoundB2(Sound::Siren);
  • 运行效率

因为lambda式是常规的函数调用,编译器大概率会将其内联,但是​​std::bind​​​调用的是指向​​setAlarm​​​的函数指针,由于编译器不太会内联通过函数指针发起的函数调用,所以​​std::bind​​调用大概率不会被内联。

1.2 对​​auto&&​​​类型的形参使用​​decltype​​​并使用​​std::forward​​传递

示例如下,下面的代码使用​​decltype​​​来推断​​x​​​的类型,因为不能使用模版中的​​T​​​,但是使用​​decltype​​​可以推断出​​x​​的类型。

template<typename T>
void normalize(T param){}

template<typename T>
void func(T param){}

//lambda生成的闭包类
class ClosureClass{
public:
template<typename T>
auto operator()(T x) const{
return func(normalize(x));
}
};

int main(){
auto f = [](auto&& x){
return func(normalize(std::forward<decltype(x)>(x)));
};
}

补充知识:

​decltype​​:

  • 如果传入左值,将会产生左值引用,如果传入右值,将会产生右值引用。

​std::forward​​:

  • 如果传递的形参是左值,将会产生左值引用的形参,如果传递的形参是右值,将会产生右值引用的形参。
  • 形参类型为左值引用,表示想返回左值;形参类型为非引用类型时,表示想返回右值。

如果​​decltype​​​绑定的是右值,那么将会产生右值引用,而非​​std::forward​​​惯例产生的右值。不过经过​​std::forward​​转发后,最后的结果都是一样的。

​forward​​源代码:

template<typename T>
T&& forward(typename std::remove_reference<T>::type& param){
return static_cast<T&&>(param);
}
//T取TestWidget时

TestWidget&& forward(TestWidget& param){
return static_cast<TestWidget&&>(param);
}

//T取TestWidget&&时
TestWidget&& && forward(TestWidget& param){
return static_cast<TestWidget&& &&>(param);
}
//引用折叠后的结果
TestWidget&& forward(TestWidget& param){
return static_cast<TestWidget&&>(param);
}

2 注意事项

2.1 避免默认捕获方式

  • 1,如果按引用捕获,可能会导致空悬引用。


解释: 按引用捕获会导致闭包内包含指向局部变量的引用或者是指涉到定义lambda式的作用域内的形参引用,一旦lambda式所创建的闭包超过了该局部变量或形参的声明周期,那么闭包的引用就会悬空。


下面的例子中,使用lambda表达式,使用了局部变量​​divisor​​​的引用,当离开​​addDivisorFilter​​​作用域后,​​divisor​​​即被销毁,导致引用悬空,在​​main​​​函数中的调用,​​filters[0](12)​​也出现了错误的值。

int computerSomevalue1(){return 2;}
int computerSomevalue2(){return 3;}
int computerDivisor(int v1, int v2){return v1 + v2;}

using FilterContainer = std::vector<std::function<bool(int)>>;
FilterContainer filters;
void addDivisorFilter(){
auto calc1 = computerSomevalue1();
auto calc2 = computerSomevalue2();
auto divisor = computerDivisor(calc1, calc2);
filters.emplace_back([&](int value){//筛选divisor的倍数
return value % divisor == 0;
});
}

int main(){
addDivisorFilter();
std::cout<<filters[0](12)<<std::endl;//1
std::cout<<(12 % 5)<<std::endl;//0
}

解决方法,就是让局部变量或者形参的声明周期更长,不过如果该lambda式被复制粘贴到其他闭包【生命周期比​​divisor​​更长】,同样也会出现同样的问题。

template<typename C>
void workWithContainer(const C& container){
auto calc1 = computerSomevalue1();
auto calc2 = computerSomevalue2();
auto divisor = computerDivisor(calc1, calc2);

using containerType = typename C::value_type;
using std::begin;
using std::end;

if(std::all_of(begin(container), end(container), [&](const containerType& value)
{return value % divisor == 0;})
){
//如果容器内的元素都是divisor的倍数
std::cout<<"All container satisfy";
}else{
std::cout<<"Not all container satisfy";
}
}


int main(){

std::vector<int> v{2, 5, 10};
workWithContainer(v);
}

另一种解决的方法就是使用值捕获的方式来替代,方法见下。

filters.emplace_back([=](int value){
return value % divisor == 0;
});
  • 2,按值的默认捕获极易受到空悬指针的影响(尤其是this),并误导人们认为它是自洽的【不受闭包外数据变化影响】。

下面的代码中,由于捕获只能针对创建lambda式的作用域可见的非静态局部变量(包括形参),而​​divisor​​​是​​TestWidget​​的成员变量,故无法捕获。

using FilterContainer = std::vector<std::function<bool(int)>>;
FilterContainer filters;

class TestWidget{
public:
void addFilter()const;
private:
int divisor;
};

void TestWidget::addFilter() const {
filters.emplace_back([](int value){
return value % divisor == 0;
});
}

int main(){
TestWidget w;
w.addFilter();
}

按下面的代码也无法捕获,因为​​divisor​​既不是局部变量也不是形参。

void TestWidget::addFilter() const {
filters.emplace_back([divisor](int value){
return value % divisor == 0;
});
}

上面的代码在编译器看来等同于下面的代码,因为每个非静态成员函数中都持有一个​​this指针​​​,每当使用该类的成员变量都会使用​​this指针​​​,也就是捕获的实际上​​TestWidget​​​的​​this指针​​​,而不是​​divisor​​。

void TestWidget::addFilter() const {
auto currentObjectPtr = this;
filters.emplace_back([divisor](int value){
return value % currentObjectPtr->divisor == 0;
});
}

采用将想要捕获的变量赋值到局部变量中,也可以让程序正常运行,或者使用广义lambda捕获。

void TestWidget::addFilter() const {
auto divisorCopy = divisor;
filters.emplace_back([=](int value){
return value % divisorCopy == 0;
});
}

//广义lambda捕获
void TestWidget::addFilter() const {
filters.emplace_back([divisor=divisor](int value){
return value % divisor == 0;
});
}

下面的代码使用了lambda默认值捕获,可能会人们以为闭包与闭包外的数据变化是绝缘的,但是lambda式还可以使用静态期存储对象【即使用static修饰的对象】,这样的对象虽然可以被lambda式使用,但是不能被捕获。也就是从结果看本来是想求5的倍数,但是静态变量被意外修改了,因此后面变成求6的倍数了。

int computerSomevalue1(){return 2;}
int computerSomevalue2(){return 3;}
int computerDivisor(int v1, int v2){return v1 + v2;}

using FilterContainer = std::vector<std::function<bool(int)>>;
FilterContainer filters;

void addDivisorFilter(){
static auto calc1 = computerSomevalue1();
static auto calc2 = computerSomevalue2();
static auto divisor = computerDivisor(calc1, calc2);

filters.emplace_back([=](int value){
return value % divisor == 0;
});
divisor++;
}

int main(){
addDivisorFilter();
std::cout<<filters[0](10)<<std::endl;
std::cout<<filters[0](12)<<std::endl;
}