为何线程安全栈的pop函数不能直接返回std::make_shared<T>(std::move(data.top()))?
这个问题的核心在于栈状态的一致性和异常安全两个关键层面,咱们逐一拆解:
1. 违背pop函数的语义,栈状态未正确更新
pop函数的核心职责是「移除栈顶元素并返回它」,但直接返回std::make_shared<T>(std::move(data.top()))的话,你只完成了「返回元素内容」的部分,栈顶元素本身并没有被从栈中移除。
举个实际例子:假设栈里有一个std::string元素"hello",调用这个错误的pop后:
- 你拿到了一个持有
"hello"的shared_ptr<std::string>; - 但栈里依然保留着那个被
std::move掏空的std::string对象(它处于「有效但未指定状态」,比如可能是空字符串)。
后续再调用top()或pop()时,你会访问这个已经被掏空的对象,这属于未定义行为,可能导致程序崩溃或逻辑混乱。
2. 无法保证异常安全
就算你想着补加data.pop(),比如写成下面这种「看起来合理」的代码:
// 错误的尝试:把构造和pop强行放一起 std::shared_ptr<T> pop() { std::lock_guard<std::mutex> lock(m); if(data.empty()) throw empty_stack(); return std::make_shared<T>(std::move(data.top())), data.pop(); }
这里的问题是:如果std::make_shared<T>(std::move(data.top()))抛出异常(比如T的移动构造函数抛出异常),那么data.pop()根本不会被执行。此时栈顶元素已经被move过(状态不确定),却依然留在栈中,栈的状态完全不一致——元素内容被移走了,但元素本身还在栈里,后续操作必出问题。
而原书的正确写法是先确保元素被安全保存到shared_ptr中,再修改栈的状态:
std::shared_ptr<T> pop() { std::lock_guard<std::mutex> lock(m); if(data.empty()) throw empty_stack(); // 先构造shared_ptr,确保元素被安全持有 std::shared_ptr<T> res(std::make_shared<T>(std::move(data.top()))); // 只有当shared_ptr构造成功后,才移除栈顶元素 data.pop(); return res; }
这种写法保证了:只有当元素已经被成功转移到shared_ptr的管理下,才会修改栈的状态。如果构造shared_ptr时抛出异常,栈的状态不会被改变(栈顶元素还在,即使被move过,至少元素本身还在栈里,后续可以尝试再次操作),不会出现「元素丢了,栈状态还不对」的情况。
额外补充:如果T的移动构造可能抛异常?
如果T的移动构造函数可能抛出异常,原书的写法其实还有优化空间——可以先把栈顶元素pop到局部变量,再构造shared_ptr:
std::shared_ptr<T> pop() { std::lock_guard<std::mutex> lock(m); if(data.empty()) throw empty_stack(); T temp = std::move(data.top()); data.pop(); return std::make_shared<T>(std::move(temp)); }
这样的话,即使构造shared_ptr时抛异常,栈已经完成了pop操作,但元素保存在局部变量temp里,虽然调用者没拿到,但栈的状态是一致的(栈顶元素已被移除)。不过这种情况属于「元素丢失但栈状态一致」,比「栈状态不一致」要好得多,具体选择取决于你的异常安全需求。
内容的提问来源于stack exchange,提问作者Alan Zeng

