A fixed size array with a head pointer and a tail pointer.
When you write to the buffer you advance the head pointer. When you read from the buffer you advance the tail pointer and erase the element that was there.
WHEN EITHER POINTER REACHES THE END OF THE ARRAY, THEY GO BACK TO THE FRONT OF THE ARRAY
Benefits:
- Fixed memory size
What happens when head catches up to tail
Two ways:
1. Head overwrites value at tail, tail moves up to always be in front of head
This means that old data is discarded. And we don’t block either reader or writer. BUT operations on the tail are now shared by both the reader and the writer.
2. Head waits for tail to advance
Old data is kept until processed (lossless). But writer can be blocked by reader. The benefit here is that writer owns the head and reader owns the tail. So no lock is needed.
Initial state (empty):
[_, _, _, _]
^head
^tail
head == tail means empty
Write A:
[A, _, _, _]
^head
^tail
Write B:
[A, B, _, _]
^head
^tail
Write C:
[A, B, C, _]
^head
^tail
Read (returns A):
[_, B, C, _]
^head
^tail
Write D:
[_, B, C, D]
^head (wraps to 0)
^tail
Write E:
[E, B, C, D]
^head
^tail
Buffer is now full. head == tail means empty, but we just wrote...
#include <atomic>
#include <optional>
#include <array>
template<typename T, size_t N>
class RingBuffer {
std::array<T, N> buffer_;
std::atomic<size_t> head_{0}; // writer owns this
std::atomic<size_t> tail_{0}; // reader owns this
size_t next(size_t current) const {
return (current + 1) % N;
}
public:
// Called by producer thread only
bool try_push(const T& value) {
size_t current_head = head_.load(std::memory_order_relaxed);
size_t next_head = next(current_head);
// Full if advancing head would hit tail
if (next_head == tail_.load(std::memory_order_acquire)) {
return false; // Buffer full, reject
}
buffer_[current_head] = value;
head_.store(next_head, std::memory_order_release);
return true;
}
// Called by consumer thread only
std::optional<T> try_pop() {
size_t current_tail = tail_.load(std::memory_order_relaxed);
// Empty if tail equals head
if (current_tail == head_.load(std::memory_order_acquire)) {
return std::nullopt; // Buffer empty
}
T value = buffer_[current_tail];
tail_.store(next(current_tail), std::memory_order_release);
return value;
}
bool empty() const {
return head_.load(std::memory_order_acquire) ==
tail_.load(std::memory_order_acquire);
}
};