For detailed documentation, see https://barometz.github.io/ringbuf/.
This is a header-only ring/circular buffer implementation in C++11, with the following goals:
- Reasonably performant.
- Readable.
- STL-compatible and STL-like.
There are two implementations: baudvine::RingBuf
and baudvine::DequeRingBuf
.
They're meant to be functionally equivalent (mostly), but behave differently
under water. Tests ideally cover both, expecting the same behaviour. Both behave
like double-ended queues up until the point where they fill up, at which point
pushing/emplacing at the front causes the element in the back to fall off (and
vice versa).
baudvine::RingBuf
is the primary implementation: it stores elements in a
fixed-size, dynamically allocated array, and provides the usual container
operations as well as baudvine::copy
for efficient copying.
baudvine::DequeRingBuf
is included primarily for testing purposes. Because
it's based on std::deque
it's a bit less efficient (time, memory
fragmentation) than the array-based one, but it's also a lot easier to trust
since all the hairy math and allocation happens in the standard library.
#include <baudvine/ringbuf.h>
void demo()
{
baudvine::RingBuf<std::string, 2> buffer;
buffer.push_back("Testing, ");
buffer.push_back("one, ");
buffer.push_back("two.");
assert(buffer.front() == "one, ");
assert(buffer.back() == "two.");
}
See test/examples.cpp for further usage examples. The
high-level pitch is this: you can use it as an STL container, except it wraps
like a ring buffer. You get iterators, front()
, back()
, push_back()
,
pop_front()
, emplace_back()
, range-for, all* the things you expect from a
standard library container.
The project is header-only, so you only have to copy
include/baudvine/ringbuf.h
into your project. You should also be able
to include it as a CMake subproject, but that's untested.
To build and run the tests, install CMake, then run:
mkdir build
cmake -S . -B build
cmake --build build
build/test/ringbuf-test
Or use your editor or IDE with CMake integration of choice.
What can't it do? Well:
- Support for user-defined/provided allocators.
- Reverse iterators.
- Generic tests for the iterators of the different implementations.
- Doesn't quite implement SequenceContainer.
- The tests for Container aren't as well organized as they could be either.
- Needs more
cowbellnoexcept
- Moved-from RingBuf instances can't be reused.
- The iterator is not std::random_access_iterator.
- Docs.
- Better code examples.
- Document iterator invalidation.
- Compare to
boost::circular_buffer
-
Special case for emplace/push members when T's lifetime doesn't matter (e.g. int)
The performance benefit turned out to be hard to measure -std::allocator_traits
is probably reasonably smart about this. - Compiler warning cleanup
- Compare to boost's spsc queue
What won't it do?
- Don't want to bother with optional features based on C++ version. Strictly C++11.
- Runtime checks are limited to the occasional
assert
. - Doesn't have
insert()
or range constructors (RingBuf(other.begin(), other.end())
), because that'd require choosing what to discard in case of overflow.
- It's meant to be C++11-compatible without a lot of optional chunks for newer standards, but maybe we can show that it does match the relevant C++20 iterator concepts.
- An implementation with mutable capacity (drop the template parameter, add
a ctor parameter and a
set_capacity
member) would be slightly slower and slightly nicer to use. - DequeRingBuf could possibly be a container adapter.
- A variant of RingBuf with automatic storage duration (so no dynamic allocation) is possible. Would be nice for embedded targets.
The tests in test/test_speed.cpp perform a number of
comparisons between RingBuf
and DequeRingBuf
. Some typical (but completely
unscientific) results:
- Hardware: Intel Core i5-7600 @ 3.5 GHz
- GCC: 12.2,
-O3 -DNDEBUG
- Clang: 14.0,
-O3 -DNDEBUG
Name | Description | RingBuf (GCC) (ms) | RingBuf (Clang) (ms) | DequeRingBuf (GCC) (ms) | DequeRingBuf (Clang) (ms) |
---|---|---|---|---|---|
PushBackToFull | push_back until the buffer is filled to capacity (225 elements) |
106 | 108 | 353 | 322 |
PushBackOverFull | push_back 225 times on a buffer with capacity 3 |
119 | 105 | 138 | 195 |
IterateOver | range-for over a buffer with 225 elements | 83 | 57 | 77 | 72 |
PushBackToFull is completely unfair because std::deque
has to allocate
memory much more frequently. In a debug build, the results are roughly
proportional (~10x).
A functional requirement of baudvine::Ringbuf
is that element lifetime is
controlled by the push
and pop
member functions: a T is constructed when it
is pushed into the buffer, and it is destroyed when popped. The tests using the
Instance
class in tests/test_ringbuf.cpp
demonstrate this.
Neither C-style arrays nor std::array
allow for this:
std::array<std::string, 5>
default-initializes five strings (calling its
default constructor), and if you destroy it in-place it'll get re-destroyed when
the array goes out of scope.
See also https://barometz.github.io/ringbuf/design-notes.html#memory-allocation.
No underlying container would provide all three of:
- Single allocation (
std::array
,std::vector
) - Push and pop on opposing ends (
std::deque
,std::list
) - Control over element lifetime (
std::vector
,std::list
,std::deque
)
See also: https://barometz.github.io/ringbuf/design-notes.html#not-an-adapter.
I chose the MIT license for this project, because I have a bad habit
of wanting to be useful and MIT's a good one for maximizing that paperclip.
One exception: deque_ringbuf.h
is MIT-0, which is effectively public domain.