《Effective Modern C++》中ThreadRAII默认移动赋值是否存在错误?
《Effective Modern C++》Item37中ThreadRAII实现的问题分析
书中的原始ThreadRAII实现
《Effective Modern C++》的Item37展示了ThreadRAII类的实现,其核心是在析构时对内部持有的std::thread执行join或detach操作,代码如下:
#include <thread> class ThreadRAII { public: enum class DtorAction { join, detach }; ThreadRAII(std::thread&& t, DtorAction a) : action(a) , t(std:: move(t)) {} ~ThreadRAII() { if(t.joinable()) { if(action == DtorAction::join) t.join(); else t.detach(); } } ThreadRAII(ThreadRAII&&) = default; ThreadRAII& operator=(ThreadRAII&&) = default; std::thread& get() { return t; } private: DtorAction action; std::thread t; }; int main() { ThreadRAII t{std::thread{[]{}}, ThreadRAII::DtorAction::join}; t = ThreadRAII{std::thread{[]{}}, ThreadRAII::DtorAction::detach}; return 0; }
作者提到,由于显式声明了析构函数,编译器不会自动生成移动操作,因此建议使用= default来实现移动构造函数和移动赋值运算符。
原始实现的问题
运行上述代码时会触发std::terminate终止程序,原因在于:
- 默认移动构造函数是可行的:它会正确移动
std::thread对象和action枚举值,原对象的std::thread会被置为不可joinable状态,析构时不会引发问题。 - 默认移动赋值运算符存在缺陷:它会直接对
std::thread执行移动赋值,而std::thread的移动赋值要求目标线程必须处于不可joinable状态——如果当前对象持有的线程还可joinable,移动赋值会直接调用std::terminate终止程序。原始实现的默认移动赋值未先处理当前对象的可joinable线程资源,导致程序崩溃。
修正后的ThreadRAII实现
针对该问题,修正后的实现需要在移动赋值时先释放当前对象的线程资源,再执行移动操作,代码如下:
#include <thread> class ThreadRAII { public: enum class DtorAction { join, detach }; ThreadRAII(std::thread&& t, DtorAction a) : action(a) , t(std:: move(t)) {} ~ThreadRAII() { release(); } ThreadRAII(ThreadRAII&&) = default; ThreadRAII& operator=(ThreadRAII&& rhs) { release(); action = rhs.action; t = std::move(rhs.t); return *this; } std::thread& get() { return t; } void release() { if(t.joinable()) { if(action == DtorAction::join) t.join(); else t.detach(); } } private: DtorAction action; std::thread t; }; int main() { ThreadRAII t{std::thread{[]{}}, ThreadRAII::DtorAction::join}; t = ThreadRAII{std::thread{[]{}}, ThreadRAII::DtorAction::detach}; return 0; }
修正核心点:
- 提取
release()方法统一处理线程的join/detach逻辑,析构函数复用该方法。 - 自定义移动赋值运算符,先调用
release()释放当前对象的线程资源(确保std::thread处于不可joinable状态),再移动赋值action和std::thread对象。
结论
书中建议使用= default实现移动赋值运算符的做法确实存在错误,因为它没有考虑std::thread移动赋值的前置条件。用户在书籍勘误列表中未找到该问题,建议联系作者提交此勘误。
内容的提问来源于stack exchange,提问作者Hugo Costa Machado
相关产品推荐
相关产品推荐

