c++中的线程安全栈:top()和pop()的组合



在他的优秀著作c++ Concurrency In Action;(第二版,包括c++ 17) Anthony Williams讨论了线程安全栈的实现。

在此过程中,他提出了std::stack的适配器实现,其中包括将top()pop()的调用合并为一个。然而,在std::stack中,将元素分离为2个独立的函数是有原因的,即在将弹出的元素返回给调用者时进行的潜在复制在复制构造函数中引发异常时,为了避免丢失数据。返回时,元素已经被弹出,因此丢失。

代替T pop()函数,他提出了pop的其他变体,这些变体能够将元素从堆栈中移除并在一次操作中将其提供给调用者,尽管所有这些都有自己的问题。

他提出的第一个备选方案的签名是void pop(T&)。调用者传递一个对T的引用,并以这种方式获得弹出的对象。但是,这种方法带来的问题是,需要在调用pop之前构造T,这可能是一个昂贵的操作,或者可能根本无法预先构造T,因为当时可能还没有必要的数据可用。作者提到的另一个问题是T可能不是可分配的,但这在这个解决方案中是必需的。

现在我的问题:如果我们通过的是std::optional<T>&而不是T&,上面提到的问题不都能解决吗?

在这种情况下,不需要在调用pop之前构造T的实例。此外,也不再需要可赋值性,因为要返回的对象可以直接使用emplace函数构造到std::optional<T>实例中。

我在这里错过了重要的东西还是我是对的?如果我确实是对的,我很想知道为什么没有考虑到这一点(是一个很好的理由还是仅仅是一个疏忽?)

std::optional确实解决了上面提到的所有问题,并且使用它来控制生命周期可能非常有价值,尽管在

中看起来有点奇怪
std::optional<T> o;
st.pop(o);

设置o总是订婚。

也就是说,在c++ 17中使用一个愚蠢的作用域保护技巧,即使不需要no-throw-movability,也可以安全地返回T。:
T pop() {
struct pop_guard {
C &c;
int u=std::uncaught_exceptions();
~pop_guard() {if(std::uncaught_exceptions()==u) c.pop_back();}
} pg{c};
return std::move(c.back());
}

(我们当然可以测试一个抛出的移动,并在它不存在的情况下移动(可能两次)。)

然而,没有提到的是,单独的toppop允许T甚至不能移动,只要底层容器支持即可。std::stack<std::mutex>工作(与emplace,而不是push!),因为std::deque不需要可移动性。