如何避免C++存储的lambda跨线程调用时捕获的this指针失效?
本问题是该问题的延伸,二者场景相似,但原问题未涉及多线程,无法完全解答我的疑惑。
如标题所述,我在使用lambda和std::thread时遇到了现有知识无法解决的问题:我尝试将包含成员函数的std::function存储在静态类中,后续在独立的std::thread中调用。问题在于该成员函数需要引用this实例指针以修改所属类的数据成员,但lambda中引用捕获的this指针在成员函数调用时已经失效/销毁,触发了未定义行为。该问题在讨论lambda与并发的提案文档P0018R3中也有相关提及。
最小可复现示例
//Function class struct FuncWrapper { //random data members std::string name; std::string description; //I still have this problem even if this member is const std::function<void()> f; }; class FuncHolder { public: //For the FuncWrapper argument, I've tried pass by reference, const-reference, rvalue, etc. doesn't make a difference in terms of the problem (as I expect) //add a FuncWrapper object to the static holder static void add(FuncWrapper f) { return const_cast<std::vector<FuncWrapper>&>(funcs).push_back(f); } //Same here with the return //get a static FuncWrapper object by index (with bounds checking) static const std::function<void()>& get(size_t index) { return funcs.at(index).f; } private: //C++17 inline static definition; If I weren't using it I would do it the non-inline way const static inline std::vector<FuncWrapper> funcs; };
//Data member class struct Base { //This is meant to be like a constexpr; its unique for each derived type inline virtual const char* const getName() const = 0; protected: //This will be important for a solution I tried const size_t storedIdx = 0; }; class Derived : public Base { public: Derived(){ FuncHolder::add({"add5", "adds 5", [&](){increment(5);} //This is the problem-causing statement }); FuncHolder::add({"add1", "adds 1", std::bind(&Derived::increment, this, 1) //This syntax also causes the same problem }); } inline const char* const getName() const override { return "Derived"; } void increment(int amount){ //Do some stuff... privMember += amount; //Do some other stuff.. } private: int privMember = 0; }; //Class to hold the static instances of the different derived type class BaseHolder { public: //make a new object instace in the static vector template<class DerivedTy> static void init(const DerivedTy& d){ static_assert(std::is_base_of_v<Base, DerivedTy>); //make sure it's actually derived size_t idx = baseVec.size(); //get the index of the object to be added const_cast<std::vector<std::unique_ptr<Base>>&>(baseVec).emplace_back(std::make_unique<DerivedTy>(d)); //forward a new unique_ptr object of derived type const_cast<size_t&>(baseVec.at(idx)->storedIdx) = idx; //store the object's index in the object itself } ///This function is used later for one of the solutions I tried; it goes unused for now ///So, assume the passed size_t is always correct in this example ///There's probably a better way of doing this, but ignore it for the purposes of the example //get a casted reference to the correct base pointer template<class DerivedTy> static DerivedTy& getInstance(size_t derivedIdx){ return *(static_cast<DerivedTy*>(baseVec.at(derivedIdx).get())); } private: //C++17 inline static again const static inline std::vector<std::unique_ptr<Base>> baseVec{}; };
int main() { BaseHolder::init(Derived()); //add a new object to the static holder //Do stuff... std::thread runFunc([&](){ FuncHolder::get(0)(); //Undefined behavior invoked here; *this pointer used in the function being called is already destroyed }); //Main thread stuff... runFunc.join(); return 0; }
该示例不算极致精简,但我特意标注了函数存储、调用类逻辑等核心细节,方便明确问题来源。
另有几点和设计相关的关键说明如下:
- 设计预期存在多个继承
Base的派生类(如Derived1、Derived2等),但每个派生类仅存在1个实例,因此BaseHolder的所有成员均为静态。若需要调整设计,请优先满足该约束。 - 可以直接将
BaseHolder实现为模板类,编译期传入需存储的派生类类型,使用tuple替代vector,但我刻意未采用该方案,因为后续可能需要在运行时动态添加派生类类型。 - 我无法修改
f的模板类型(即std::function<>的签名),因为需要兼容不同返回值、不同入参的函数,通过lambda或std::bind包装为无返回无入参的std::function<void()>。 - 设计的核心目标是实现类似事件触发的静态调用逻辑:被调用的函数提前构造完成,调用时可修改其所属类的成员(本示例中为
Derived类的成员)。
问题来源
根据我的现有知识,lambda中引用捕获的this指针,在其他线程执行lambda时确实可能失效。调试发现甚至在Derived构造函数中构造lambda时,this指针就已经被销毁,这和我的认知不符,无法100%确认该现象的成因:调试器显示Derived的this实例全部为垃圾值,内存不可读。
Derived(){ FuncHolder::add({"add5", "adds 5", //`this` pointer is fine here [&](){increment(5);} //`this` pointer is filled with junk and pointing to a different random address }); //... }
我可以确认lambda/函数调用时触发了未定义行为,是由于Derived的this实例指针已销毁:每次运行抛出的异常类型、来源文件均不相同,有时直接触发读取访问冲突,调试器也无法读取lambda中this指针的内存,均符合指针销毁的特征。
我之前处理过无线程场景下的同类lambda问题,但多线程场景让问题变得更复杂,后续会展开说明。
值捕获
最简单的方案是使用C++17特性,让lambda值捕获Derived的this指针(前述问题的回答和P0018R3提案均提及该方案),提案中也明确提到线程等并发场景下需要值捕获this:
Derived(){ FuncHolder::add({"add5", "adds 5", [&, *this](){increment(5);} //Capture *this by value (C++17); it's thread-safe now }); //... }
但该方案的问题在于,lambda中调用的函数需要修改原类的成员,值捕获this只会修改Derived实例的副本,无法修改目标原实例。
使用静态实例
既然每个派生类只有一个静态实例,且存在静态的派生类持有者,那么直接在lambda中使用静态实例,修改该实例即可:
Derived(){ FuncHolder::add({"add5", "adds 5", [=](){BaseHolder::getInstance(storedIdx).increment(5);} //use static instance in lambda; Again assume the passed index is always correct for this example }); //... }
该方案逻辑上可行,但存在问题:getInstance()在构造函数中调用,此时实例还未完成构造。具体来说,Derived()构造函数在BaseHolder::init(Derived())中被调用,init才会将实例存入vector,而Derived()构造函数执行时init还未完成vector写入操作,访问vector会出错。
向成员函数传入静态实例
前述问题的另一个回答提到,可以修改lambda中调用的函数,新增所属类实例的入参,本示例中实现如下:
class Derived : public Base { public: Derived(){ FuncHolder::add({"add5", "adds 5", [&](){increment(BaseHolder::getInstance(storedIdx), 5);} //Pass the static instance to the actual function }); //... } //rest of the class... void increment(Derived& instance, int amount){ //Do some stuff... instance.privMember += amount; //Do some other stuff.. } private: int privMember = 0; };
但该方案和上一方案存在相同问题:构造函数执行时静态实例还未创建,访问实例会出错。
直接使用this的shared_ptr
前述问题多次提到可以使用shared_ptr(或任意智能指针)持有this,延长其生命周期(但回答未给出具体实现)。直接实现的方式如下:
Derived(){ FuncHolder::add({"add5", "adds 5", [self=std::shared_ptr<Derived>()](){self->increment(5);} //pass a shared_ptr of *this; syntax can differ }); //... }
该方案会抛出std::bad_weak_ptr异常,实现方式本身存在问题。
通过std::enable_shared_from_this<T>获取this的shared_ptr
我在无线程场景下常用的方案,以及该博客提到的方案,是继承std::enable_shared_from_this<T>,调用shared_from_this()获取合法的this的shared_ptr:
class Derived : public Base, public std::enable_shared_from_this<Derived> { Derived(){ FuncHolder::add({"add5", "adds 5", [self=shared_from_this()](){self->increment(5);} //pass a shared_ptr of *this }); //... } //rest of class... };
该方案逻辑上可行,也不会抛出异常,但实际运行问题依旧存在,和直接引用捕获this的表现没有区别。
是否可以避免lambda中派生类的this指针在跨线程调用时失效?如果不行,如何调整设计可以实现目标需求,同时保留现有的设计约束?
内容的提问来源于stack exchange,提问作者Abob

