Home > Enterprise >  Thread-safe stack in C : combined top() and pop()
Thread-safe stack in C : combined top() and pop()

Time:11-29

In his excellent book "C Concurrency in Action" (2nd edition including C 17) Anthony Williams discusses the implementation of a thread-safe stack.

In the course of this, he proposes an adapter implementation of std::stack, which, among other things, would combine the calls of top() and pop() into one. The separation into 2 separate functions, however, was done for a reason in std::stack, namely to avoid losing data in the case that a potential copy made when returning the popped element to the caller throws an exception inside the copy constructor. When returning, the element will have already been popped off and is consequentially lost.

Instead of having a function T pop(), he proposes other variations of pop that would be able to remove the element off the stack and provide it to the caller in one operation, all of which come with their own problems, though.

The 1st alternative he proposes has the signature void pop(T&). The caller passes in a reference to a T and gets the popped off object that way. This way of doing it, however, comes with the problem that a T need be constructed prior to the call to pop, which might be an expensive operation, or it might not be possible to construct a T beforehand at all because necessary data might not be available yet at the time. Another problem the author mentions is that T might not be assignable, which would be required for this solution, though.

Now my question: Wouldn't all of the mentioned problems be solved if we passed a std::optional<T>& instead of a T&?

In that case, no instance of T would need to be constructed prior to the call to pop. Furthermore, assignability would not be required anymore either, since the object to be returned could be constructed into the std::optional<T> instance directly using its emplace function.

Am I missing something crucial here or am I right? If I am indeed right, I would be curious to know why this was not considered (for a good reason or just plainly an oversight?).

CodePudding user response:

std::optional does solve all of the mentioned problems, and using it to control lifetime can be quite valuable, although it would appear a bit strange in

std::optional<T> o;
st.pop(o);

to have o always engaged.

That said, with a stupid scope-guard trick it's possible in C 17 to safely return T even without requiring no-throw-movability:

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());
}

(We could of course test for a throwing move and just move (perhaps twice) in its absence.)

However, what wasn't mentioned is that separate top and pop allows a T that isn't even movable, so long as the underlying container supports it. std::stack<std::mutex> works (with emplace, not push!) because std::deque doesn't require movability.

  • Related