lambda表达式是C 11最重要也最常用的一个特性之一。lambda来源于函数式编程的概念,也是现代编程语言的一个特点。 关于lambda表达式的概念并不是本文的重点,网上可以找到无数的写得极好的文章介绍它。我想说的是善用lambda表达式,将给C 编程带来极大的便利,这是本人最近学习C 11以来真实深切的感受,但是有时候误用lambda表达式也会给编程带来极大的隐患,本文以最近的经历说明lambda表达式在使用上的一例陷阱。
一个简单的例子
下面是一段很简单的lambda测试代码。总体的功能就是让对象在析构时执行指定的std::function<void(int)>
函数对象。
test_lambda_base
类的功能很简单,就是在析构函数中执行构造函数传入的一个std::function<void()>
对象。
test_lambda
是test_lambda_base
的子类,也很简单,在构造函数中将传入的std::function<void(int)>
用lambda表达式封装成std::function<void()>
传给父类test_lambda_base
构造函数。这样,当test_lambda
的对象在析构时将会执行对象构造时指定的std::function<void(int)>
对象。
#include <iostream>
#include <functional>
using namespace std;
class test_lambda_base {
public:
test_lambda_base(std::function<void()> f):on_release(f) {
}
~test_lambda_base() {
cout << "destructor of test_lambda_base" << endl;
on_release(); //执行传入的函数对象
}
private:
std::function<void()> on_release;
};
class test_lambda:public test_lambda_base {
public:
test_lambda(std::function<void(int)> f):fun(f)
,test_lambda_base([this] {
fun(12345);
})
{
}
~test_lambda() {
cout << "destructor of test_lambda" << endl;
}
private:
std::function<void(int)> fun;
};
int main() {
test_lambda tst_lam([](int i){
cout<<i<<endl;
});
cout << "!! !Hello World!!!" << endl; // prints !!!Hello World!!!
}
在eclipse gcc(5.2)环境下编译运行,的确会输出预期的运行结果,程序结束的时候,调用了指定的lambda表达式:
!! !Hello World!!! destructor of test_lambda destructor of test_lambda_base 12345
问题来了
一切都是预期的。。。完美。。。
然而当我在VisualStudio2015下同样运行这段代码,却抛出了异常。。。仔细跟踪分析,发现当程序到下图箭头所指的位置时,test_lambda
的成员变量fun
显示是empty
。这就是异常发生的直接原因。。。
一开始我总是在纠结为什么gcc和vs2015下运行的结果不一样,既然在gcc下运行正常说明我的代码逻辑没问题,这该不会是vs2015的一个bug吧?想想也不太可能。还得从代码上找原因。 将上图箭头位置的lambda表达式的捕获列表改为[=],[&],都试过了,问题依旧:gcc下正常,vs2015下异常。
代码语言:javascript复制[=] {
fun(12345);
};
[&] {
fun(12345);
};
析构顺序
然后我想到了C 析构顺序的问题,按照C 标准,C 对象析构的顺序与构造顺序完全相反:
析构函数体->清除成员变量->析构基类部分(从右到左)->析构虚基类部分
所以上面代码中在test_lambda_base
的析构函数中执行子类test_lambda
的成员变量fun
时,fun
作为一个std::function
对象已经被析构清除了,这时fun
已经是个无效变量,执行它当然会抛出异常。
为了证实这个判断,打开头文件#include <functional>
找到function
的析构函数,如下图在析构函数上设置一个调试断点,再运行程序到断点处。
看下图中的”调用堆栈”窗口。在test_lambda
的析构函数~test_lambda
执行时,类型为std::function<void(int)>
的fun
成员的析构函数~function<void(int)>()
被执行了,所以当再执行到test_lambda_base
的析构函数时,fun
已经是无效的了。
所以前面不论将捕获列表改为[&]
还是[=]
,还是别的什么尝试都无济于事。因为问题的原因不是lambda表达捕获的this
指针不对,而是在基类的析构函数中,lambda表达式所捕获的this指针所指向的子类对象部分的数据已经无效,不可引用了。
解决问题
解决这个问题的办法很多种,
总的原则就是:如果要在析构函数中调用lambda表达,就要避免lambda使用类成员变量,
对于这个例子,最简单的办法就是修改test_lambda
构造函数,如下示例,改为将f
参数加入lambda表达捕获列表,也就是以传值方式把f
参数提供给lambda表达。
test_lambda(std::function<void(int)> f):fun(f)
,test_lambda_base([f] {
f(12345);
})
{
}
为什么gcc和vs2015下代码的表现不同?
最后一个问题:为什么gcc和vs2015下代码的表现不同?
我同样用前面在std::function
析构函数加断点的方式在eclipse gcc环境下做了测试,测试结果表明gcc也是按C 标准顺序执行对象析构的,但不同的是gcc在构造下面这个lambda表达式时,将fun
对象复制了一份,所以当代码执行到lambda表达式时,fun
并不是子类对象中已经析构的那个无效对象了。
test_lambda(std::function<void(int)> f):fun(f)
,test_lambda_base([this] {
fun(12345);//gcc下,这个fun已经不是test_lambda中的fun对象了
})
{
}
所以这代码在gcc下能正常运行算是侥幸。
总结
如果在基类的析构函数中执行子类提供lambda表达式,lambda表达式中要避免使用子类中类成员变量。因为这时子类的类成员变量已经被析构了,但是子类中的指针类型、基本数据类型变量因为不存在析构的问题所以还是可以用的。