移动含std::thread的MyClass实例时引用失效问题求解
// USING C++ 17 #include <iostream> #include <thread> #include <vector> #include <chrono> class MyClass { std::thread thread; std::string mystring; public: bool done = false; MyClass(std::string string) { mystring = string; thread = std::thread(&MyClass::worker, std::ref(*this)); thread.detach(); } void worker() { // notice how mystring has the wrong value, some of the time std::cout << "mystring is: \"" << mystring << "\"" << std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(1000)); done = true; } }; int main() { std::vector<MyClass> my_vec {}; for (int i=0; i<15; i++) { my_vec.emplace_back("hello"); } while (my_vec.size() > 0) { for (auto it = my_vec.begin(); it != my_vec.end();) { if (it->done) { it = my_vec.erase(it); } else { ++it; } } } return 0; }
需求是将MyClass实例存储在std::vector中,定期遍历并移除已完成(done为true)的实例。但调用emplace_back时,vector扩容会触发已有实例的移动构造,导致worker线程内的this引用指向旧内存,mystring值偶尔出现错误。需要保留GUI事件循环式的遍历移除模式,已尝试重写移动构造、转移线程、静态worker传引用均未解决问题。
方法1:用智能指针存储实例,避免vector移动对象
将vector的元素类型改为std::unique_ptr<MyClass>,vector扩容时仅移动指针,不会移动MyClass实例本身,worker线程的this引用始终指向正确的内存地址。
修改后代码示例:
// USING C++ 17 #include <iostream> #include <thread> #include <vector> #include <chrono> #include <memory> class MyClass { std::thread thread; std::string mystring; public: bool done = false; MyClass(std::string string) : mystring(std::move(string)) { thread = std::thread(&MyClass::worker, this); thread.detach(); } // 禁止拷贝,允许默认移动构造 MyClass(const MyClass&) = delete; MyClass& operator=(const MyClass&) = delete; MyClass(MyClass&&) = default; MyClass& operator=(MyClass&&) = default; void worker() { std::cout << "mystring is: \"" << mystring << "\"" << std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(1000)); done = true; } }; int main() { std::vector<std::unique_ptr<MyClass>> my_vec{}; for (int i = 0; i < 15; i++) { my_vec.emplace_back(std::make_unique<MyClass>("hello")); } while (!my_vec.empty()) { for (auto it = my_vec.begin(); it != my_vec.end();) { if ((*it)->done) { it = my_vec.erase(it); } else { ++it; } } } return 0; }
说明:unique_ptr托管MyClass实例,vector存储指针,扩容时仅移动指针,不会触发MyClass的移动构造,彻底避免worker线程的引用失效问题。
方法2:提前给vector预留足够空间,避免扩容移动
若能预估需要存储的MyClass实例数量,提前用reserve分配足够内存,后续emplace_back不会触发扩容,也就不会移动已有实例。
修改main函数部分:
int main() { std::vector<MyClass> my_vec{}; my_vec.reserve(15); // 提前预留15个元素的空间 for (int i=0; i<15; i++) { my_vec.emplace_back("hello"); } while (my_vec.size() > 0) { for (auto it = my_vec.begin(); it != my_vec.end();) { if (it->done) { it = my_vec.erase(it); } else { ++it; } } } return 0; }
说明:仅适用于能提前确定实例数量的场景,若后续添加实例超过预留数量,仍会触发扩容移动,局限性较大。
方法3:将核心状态存入共享智能指针,分离线程绑定的状态
把mystring和done等状态放到独立结构体中,用std::shared_ptr托管,worker线程绑定该共享指针。即使MyClass实例被移动,共享指针指向的状态内存地址不变。
修改后代码示例:
// USING C++ 17 #include <iostream> #include <thread> #include <vector> #include <chrono> #include <memory> class MyClass { std::thread thread; struct State { std::string mystring; bool done = false; }; std::shared_ptr<State> state; public: MyClass(std::string string) : state(std::make_shared<State>()) { state->mystring = std::move(string); thread = std::thread(&MyClass::worker, state); thread.detach(); } // 允许默认拷贝和移动构造 MyClass(const MyClass&) = default; MyClass& operator=(const MyClass&) = default; MyClass(MyClass&&) = default; MyClass& operator=(MyClass&&) = default; // 对外暴露done状态的访问接口 bool is_done() const { return state->done; } private: static void worker(std::shared_ptr<State> state) { std::cout << "mystring is: \"" << state->mystring << "\"" << std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(1000)); state->done = true; } }; int main() { std::vector<MyClass> my_vec{}; for (int i = 0; i < 15; i++) { my_vec.emplace_back("hello"); } while (!my_vec.empty()) { for (auto it = my_vec.begin(); it != my_vec.end();) { if (it->is_done()) { it = my_vec.erase(it); } else { ++it; } } } return 0; }
说明:线程绑定的是共享指针指向的状态,与MyClass实例的存储位置无关,无论实例如何移动,状态的内存地址始终稳定,灵活性最高,适合动态添加实例的场景。
内容的提问来源于stack exchange,提问作者eth

