46. Circular buffer

A circular buffer is a fixed-size container that behaves as if its two ends were connected to form a virtual circular memory layout. Its main benefit is that you don't need a large amount of memory to retain data, as older entries are overwritten by newer ones. Circular buffers are used in I/O buffering, bounded logging (when you only want to retain the last messages), buffers for asynchronous processing, and others.

We can differentiate between two situations:

  1. The number of elements added to the buffer has not reached its capacity (its user-defined fixed size). In this case, it behaves likes a regular container, such as a vector.

 

  1. The number of elements added to the buffer has reached and exceeded its capacity. In this case, the buffer's memory is reused and older elements are being overwritten.

We could represent such a structure using:

  • A regular container with a pre-allocated number of elements
  • A head pointer to indicate the position of the last inserted element
  • A size counter to indicate the number of elements in the container, which cannot exceed its capacity (since elements are being overwritten in this case)

The two main operations with a circular buffer are:

  • Adding a new element to the buffer. We always insert at the next position of the head pointer (or index). This is the push() method shown below.
  • Removing an existing element from the buffer. We always remove the oldest element. That element is at position head - size (this must account for the circular nature of the index). This is the pop() method shown below.

The implementation of such a data structure is shown here:

template <class T>
class circular_buffer
{
typedef circular_buffer_iterator<T> const_iterator;

circular_buffer() = delete;
public:
explicit circular_buffer(size_t const size) :data_(size)
{}

bool clear() noexcept { head_ = -1; size_ = 0; }
bool empty() const noexcept { return size_ == 0; }
bool full() const noexcept { return size_ == data_.size(); }
size_t capacity() const noexcept { return data_.size(); }
size_t size() const noexcept { return size_; }

void push(T const item)
{
head_ = next_pos();
data_[head_] = item;
if (size_ < data_.size()) size_++;
}

T pop()
{
if (empty()) throw std::runtime_error("empty buffer");
auto pos = first_pos();
size_--;
return data_[pos];
}

const_iterator begin() const
{
return const_iterator(*this, first_pos(), empty());
}

const_iterator end() const
{
return const_iterator(*this, next_pos(), true);
}

private:
std::vector<T> data_;
size_t head_ = -1;
size_t size_ = 0;

size_t next_pos() const noexcept
{ return size_ == 0 ? 0 : (head_ + 1) % data_.size(); }
size_t first_pos() const noexcept
{ return size_ == 0 ? 0 : (head_ + data_.size() - size_ + 1) %
data_.size(); }

friend class circular_buffer_iterator<T>;
};

Because of the circular nature of the indexes mapped on a contiguous memory layout, the iterator type for this class cannot be a pointer type. The iterators must be able to point elements by applying modulo operations on the index. Here is a possible implementation for such an iterator:

template <class T>
class circular_buffer_iterator
{
typedef circular_buffer_iterator self_type;
typedef T value_type;
typedef T& reference;
typedef T const& const_reference;
typedef T* pointer;
typedef std::random_access_iterator_tag iterator_category;
typedef ptrdiff_t difference_type;
public:
circular_buffer_iterator(circular_buffer<T> const & buf,
size_t const pos, bool const last) :
buffer_(buf), index_(pos), last_(last)
{}

self_type & operator++ ()
{
if (last_)
throw std::out_of_range("Iterator cannot be incremented past the end of range.");
index_ = (index_ + 1) % buffer_.data_.size();
last_ = index_ == buffer_.next_pos();
return *this;
}

self_type operator++ (int)
{
self_type tmp = *this;
++*this;
return tmp;
}

bool operator== (self_type const & other) const
{
assert(compatible(other));
return index_ == other.index_ && last_ == other.last_;
}

bool operator!= (self_type const & other) const
{
return !(*this == other);
}

const_reference operator* () const
{
return buffer_.data_[index_];
}

const_reference operator-> () const
{
return buffer_.data_[index_];
}
private:
bool compatible(self_type const & other) const
{
return &buffer_ == &other.buffer_;
}

circular_buffer<T> const & buffer_;
size_t index_;
bool last_;
};

With all these implemented, we could write code such as the following. Notice that in the comments, the first range shows the actual content of the internal vector, and the second range shows the logical content as exposed with iterator access:

int main()
{
circular_buffer<int> cbuf(5); // {0, 0, 0, 0, 0} -> {}

cbuf.push(1); // {1, 0, 0, 0, 0} -> {1}
cbuf.push(2); // {1, 2, 0, 0, 0} -> {1, 2}
cbuf.push(3); // {1, 2, 3, 0, 0} -> {1, 2, 3}

auto item = cbuf.pop(); // {1, 2, 3, 0, 0} -> {2, 3}
cbuf.push(4); // {1, 2, 3, 4, 0} -> {2, 3, 4}
cbuf.push(5); // {1, 2, 3, 4, 5} -> {2, 3, 4, 5}
cbuf.push(6); // {6, 2, 3, 4, 5} -> {2, 3, 4, 5, 6}

cbuf.push(7); // {6, 7, 3, 4, 5} -> {3, 4, 5, 6, 7}
cbuf.push(8); // {6, 7, 8, 4, 5} -> {4, 5, 6, 7, 8}

item = cbuf.pop(); // {6, 7, 8, 4, 5} -> {5, 6, 7, 8}
item = cbuf.pop(); // {6, 7, 8, 4, 5} -> {6, 7, 8}
item = cbuf.pop(); // {6, 7, 8, 4, 5} -> {7, 8}

item = cbuf.pop(); // {6, 7, 8, 4, 5} -> {8}
item = cbuf.pop(); // {6, 7, 8, 4, 5} -> {}

cbuf.push(9); // {6, 7, 8, 9, 5} -> {9}
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset