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

本文通过一个实例探讨了在C++11中使用Lambda表达式时可能遇到的问题,特别是在基类析构函数中调用子类提供的Lambda表达式,并提出了解决方案。

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

<think>我们正在讨论C++std::thread与lambda表达式的结合使用,特别是捕获this指针的用法。 根据引用[1]和[3],lambda表达式C++11中引入,特别适合在回调等紧密关联的函数关系或短小且不复用的函数中使用。 用户的问题:理解C++中使用lambda表达式作为线程函数的std::thread用法,特别是捕获this的用法。 回答结构: 1. 简要介绍std::thread和lambda表达式结合使用的背景。 2. 详细解释lambda捕获this的用法,包括为什么需要捕获this以及如何捕获。 3. 给出代码示例,展示如何在线程中使用lambda捕获this指针。 4. 讨论注意事项(如生命周期问题)。 5. 生成相关问题。 注意:用户引用了四段内容,其中引用[1]提到C++11Lambda表达式,引用[3]提到lambda的适用场景,引用[4]提到线程池(可能相关),但用户问题集中在std::thread和lambda捕获this上。 因此,我们将重点放在捕获this上。 回答: 在C++中,std::thread可以接受一个可调用对象作为线程函数,lambda表达式是常用的选择。当lambda在类成员函数内部定义,并且需要访问类的成员变量或函数时,需要捕获this指针捕获this后,lambda体内就可以访问当前对象的成员。 捕获方式: - 按值捕获:`[this]` 或 `[=]`(C++11/14,但后者会捕获所有局部变量,不推荐) - 按引用捕获:`[&]` 或 `[&, this]`(但注意,按引用捕获this实际上还是指针的副本,与按值捕获this效果相同,因为this本身是指针。但按引用捕获其他局部变量有风险,因为线程可能延长生命周期) 重要:捕获this后,必须确保线程执行期间,该对象仍然存在(即对象生命周期不能结束)。否则,访问已销毁的对象会导致未定义行为。 示例代码: 假设有一个类,我们想在一个新线程中调用其成员函数。 ```cpp #include <iostream> #include <thread> class MyClass { public: void startThread() { // 使用lambda表达式创建线程,并捕获this thread_ = std::thread([this] { this->workerFunction(); // 或者直接调用成员函数 // 或者直接访问成员变量 std::cout << "Inside thread, member: " << member_ << std::endl; }); } void joinThread() { if (thread_.joinable()) { thread_.join(); } } private: std::thread thread_; int member_ = 42; void workerFunction() { std::cout << "Worker function called." << std::endl; } }; int main() { MyClass obj; obj.startThread(); obj.joinThread(); return 0; } ``` 注意事项: 1. 生命周期:确保对象在线程运行期间有效。如果对象在栈上创建,并且线程在对象销毁后仍在运行,则访问this指向的对象会出错。 2. 线程管理:需要确保线程在对象销毁前被正确join或detach。通常,在析构函数中需要检查线程是否可join,并决定是等待线程结束(join)还是分离(detach)。但分离后线程会独立运行,可能访问已销毁的对象,因此需谨慎。 3. 按值捕获this与按引用捕获this:实际上,捕获this都是按值捕获指针(即指针的副本)。因此,在lambda内部使用this时,它指向的是创建lambda时的对象。但要注意,如果对象被移动(例如),那么this指针可能不再有效。 另一种常见用法是,在lambda中直接调用成员函数,或者访问成员变量。注意,如果成员函数是虚函数,在lambda中调用时,会根据对象的实际类型进行动态绑定。 关于线程池(引用[4]提到),虽然与用户问题不直接相关,但线程池中提交任务时也常使用lambda表达式,并且可能捕获this(当任务与对象相关时)。此时同样需要注意对象生命周期问题。 生成相关问题: 1. 在C++中,如何确保使用lambda的线程安全地访问类成员? 2. std::thread和lambda结合使用时,如何避免悬垂引用? 3. 在C++多线程编程中,除了lambda,还有哪些方式可以传递线程函数? 4. 当使用lambda捕获this时,如何管理线程的生命周期以避免访问已销毁的对象? 注意:用户要求生成相关问题,我们生成了4个。 最后,根据引用[3]的建议,lambda适合在函数体较短且不复用的场景,因此在线程函数中,如果逻辑简单,使用lambda是合适的。</think>### C++ std::thread 结合 Lambda 表达式捕获 `this` 的用法详解 在 C++ 多线程编程中,`std::thread` 与 Lambda 表达式结合使用时,通过捕获 `this` 指针可以实现对当前类成员的访问。以下是关键要点和示例: #### 1. **基本语法与原理** - **捕获 `this`**:Lambda 表达式通过 `[this]` 捕获当前对象的指针 - **访问成员**:在 Lambda 体内可直接访问类的成员变量和函数 ```cpp class MyClass { public: void startThread() { // 捕获 this 指针访问成员 thread_ = std::thread([this] { this->member_func(); // 调用成员函数 member_var_ = 42; // 修改成员变量 }); } private: std::thread thread_; int member_var_; }; ``` #### 2. **生命周期注意事项** - **对象销毁风险**:若线程运行时对象已被销毁,会导致未定义行为(悬垂指针) - **解决方案**: - 在对象析构函数中确保线程已结束(通过 `join()` 或 `detach()`) ```cpp ~MyClass() { if (thread_.joinable()) { thread_.join(); // 等待线程结束 } } ``` #### 3. **完整示例代码** ```cpp #include <iostream> #include <thread> class Worker { public: Worker(int id) : id_(id) {} void start() { thread_ = std::thread([this] { // 通过 this 访问成员 std::cout << "Thread " << id_ << " started\n"; process(); // 调用成员函数 }); } ~Worker() { if (thread_.joinable()) { thread_.join(); // 确保线程安全结束 } } private: int id_; std::thread thread_; void process() { std::cout << "Processing in thread " << id_ << "\n"; } }; int main() { Worker w(1); w.start(); return 0; } ``` #### 4. **常见问题与解决方案** | 问题 | 原因 | 解决方案 | |------|------|----------| | 悬垂指针 | 对象销毁后线程仍在运行 | 在析构函数中调用 `join()` | | 数据竞争 | 多线程同时修改成员变量 | 使用 `std::mutex` 同步访问 | | 性能开销 | Lambda 捕获大对象 | 按需捕获(如 `[this, &specific_var]`) | #### 5. **最佳实践** 1. **最小化捕获**:优先使用 `[this]` 而非 `[=]` 或 `[&]`,避免意外捕获 2. **线程同步**:访问共享成员时使用互斥锁: ```cpp std::mutex mtx_; [this] { std::lock_guard<std::mutex> lock(mtx_); // 安全访问共享数据 } ``` 3. **避免分离线程**:除非明确保证对象生命周期,否则慎用 `detach()` > **关键提示**:Lambda 表达式特别适合短小且与当前对象紧密关联的异步任务(如回调),但需严格管理生命周期[^3]。 --- ### 相关问题 1. 如何在 Lambda 表达式捕获特定成员变量而非整个 `this` 指针? 2. 当使用 `std::thread` 和 Lambda 时,如何避免多线程环境下的数据竞争? 3. C++Lambda 捕获 `this` 与线程池结合使用有哪些典型场景? 4. 如果需要在多个线程中共享同一个对象,如何设计安全的析构机制?
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

10km

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

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

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

打赏作者

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

抵扣说明:

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

余额充值