RAII equivalent for FIFO release order

Viewed 91

RAII is quite comfortable and I am having difficulties to provide an equivalent design for resources that must be released in the same order they were acquired (FIFO) as opposed to in the reverse order (Stack) that naturally come out of RAII.

In my specific case, I have a stream class as follow:

template<typename T>
class stream {
  ...
public:
  // Producer API
  T& write_acquire(); // This acquires a storage element and will "block"
                      // until a slot is available in the underlying resource
  void write_release(&T); // This releases the storage element, transferring the data to a consumer

  // Consumer API
  T& read_acquire(); // This acquires a storage element and will "block"
                     // until a slot has been write_release
  void read_release(&T); // This releases the storage element making it available
                         // for a potential future write_acquire
};

And I was thinking of providing a RAII-style helper:

template<typename T>
class stream_wslot {
  stream<T> &s;
  T &slot;
public:
  stream_wslot(stream<T> &s) : s{s}, slot{s.write_acquire()} {}
  ~stream_wslot() { s.write_release(slot); }

  operator T&() { return slot; }
  T& operator=(T &val) { return slot = val; }
};

But the issue is the following usage will not behave correctly:

void test(stream<float> &fifo) {
  stream_wslot even(fifo);
  stream_wslot odd(fifo);
  ... first ...
  ... second ...
  // releases odd !!!
  // releases even
}

That is, we will release the odd slot before we release the even slot. While I could add a "reordering" queue in the stream I was wondering if there was a "clean" way of generalizing RAII to those situations.

3 Answers

Using std::optional, at a cost of very modest, minimal overhead provides more control over construction, and a well-defined destruction order. Which is exactly what you're looking for, here.

For example:

std::optional<stream_wslot> o_even;
std::optional<stream_wslot> o_odd;

o_odd.emplace(fifo);
o_even.emplace(fifo);

auto &even=*o_even;
auto &odd=*o_odd;

From this point on, existing code that uses odd and even will find it hard to tell the difference. And the grand total: odd gets constructed first, even gets constructed second. odd gets destroyed first, when leaving this scope, even gets destroyed second. Same effective construction and destruction order.

You could have a queue running on another thread for implementing the FIFO logic asynchronously. If wslot objects are constructed with a reference to that queue, they can tell it to perform write_release() for them when they destruct, but perform a regular write_acquire() on construction, which would block if necessary.

(Alternatively, on construction, they could pass an std::future somewhere, and fulfill the promise by write_release()ing on destruction. But this is just vague hand-waving on my part).

Here's an alternative that uses a std::queue and works in C++11 and up:

#include <queue>

void test(stream<float> &fifo) {
    std::queue<stream_wslot> wslots;

  #if __cplusplus >= 201703L                             // C++17 and later:
    auto& even = wslots.emplace(fifo);
    auto& odd = wslots.emplace(fifo);
  #else                                                  // C++11 and C++14:
    auto& even = (wslots.emplace(fifo), wslots.front());    
    auto& odd = (wslots.emplace(fifo), wslots.front());
  #endif

    // work with even and odd ...
}

The element referenced by even is created first, then the element referenced by odd.

At scope end, the std::queue first releases the element referenced by even and then releases the element referenced by odd.

Related