C++11 在析构函数中执行lambda表达式(std::function)捕获this指针的陷阱

lambda表达式是C++11最重要也最常用的一个特性之一。lambda来源于函数式编程的概念,也是现代编程语言的一个特点。
关于lambda表达式的概念并不是本文的重点,网上可以找到无数的写得极好的文章介绍它。我想说的是善用lambda表达式,将给C++编程带来极大的便利,这是本人最近学习C++11以来真实深切的感受,但是有时候误用lambda表达式也会给编程带来极大的隐患,本文以最近的经历说明lambda表达式在使用上的一例陷阱。

一个简单的例子

下面是一段很简单的lambda测试代码。总体的功能就是让对象在析构时执行指定的std::function<void(int)>函数对象。
test_lambda_base 类的功能很简单,就是在析构函数中执行构造函数传入的一个std::function<void()>对象。
test_lambdatest_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下异常。

[=] {
		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表达式中要避免使用子类中成员变量。因为这时子类的类成员变量已经被析构了,但是子类中的指针类型、基本数据类型变量因为不存在析构的问题所以还是可以用的。

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

10km

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值