《C++ Concurrency In Action》167页代码疑问:为何需先赋值右值给const变量?
首先咱们先把两个版本的代码摆出来对照:
原代码:
std::shared_ptr<T> wait_and_pop() { std::unique_ptr<node> const old_head=wait_pop_head(); return old_head->data; }
你想问的简写版本:
std::shared_ptr<T> wait_and_pop() { return wait_pop_head()->data; }
要搞懂为什么原代码要多一步赋值给const变量,得先明确几个关键点:wait_pop_head()返回的是std::unique_ptr<node>——这是个独占所有权的智能指针,它销毁时会自动delete指向的node对象;而node内部的data是std::shared_ptr<T>。
先看简写版本的潜在问题
从表面上看,简写代码好像能正常工作:调用wait_pop_head()拿到临时的unique_ptr,访问它的data成员,拷贝这个shared_ptr后返回,然后临时unique_ptr销毁、delete掉node。
但这里有个隐藏的风险:根据C++的规则,临时对象的生命周期会在包含它的完整表达式结束时销毁。这里的完整表达式就是return wait_pop_head()->data;这一行。也就是说,node对象的销毁时间是在return语句完成的瞬间——虽然大多数情况下,shared_ptr的拷贝已经完成,不会影响T对象的存活,但如果node::data是某种特殊类型(比如不是直接的shared_ptr,而是需要依赖node对象存在才能完成拷贝的自定义智能指针),那临时unique_ptr销毁后,node被释放,拷贝data的操作就可能访问到已释放的内存,触发未定义行为。
原代码这么写的原因
明确所有权,保证生命周期安全
把临时的unique_ptr赋值给命名变量old_head后,old_head的生命周期会持续到函数结束(也就是return语句执行完之后)。这就确保了在我们读取、拷贝data的整个过程中,node对象始终是有效的,完全杜绝了上述的潜在风险。代码可读性与严谨性
用命名变量代替嵌套调用,能清晰地告诉阅读代码的人:我们需要持有这个头节点的所有权,直到完成data的获取。在并发编程场景下,这种明确的内存管理逻辑能减少误解,降低出错概率。用const强化安全性
给old_head加上const修饰,是为了防止我们不小心修改这个指针(比如误操作调用reset())——毕竟我们只需要读取data,不需要改变头指针的状态,这是一种防御性编程的好习惯。
所以说,原代码的写法是一种更严谨、更安全的编程风格,虽然在很多场景下简写版本能跑通,但原代码彻底避免了边界情况的风险,同时让代码意图更清晰。
内容的提问来源于stack exchange,提问作者Alan Zeng

