diff --git a/include/xo/alloc/CircularBuffer.hpp b/include/xo/alloc/CircularBuffer.hpp new file mode 100644 index 00000000..cf1729ce --- /dev/null +++ b/include/xo/alloc/CircularBuffer.hpp @@ -0,0 +1,245 @@ +/* CircularBuffer.hpp + * + * author: Roland Conybeare, Aug 2025 + */ + +#pragma once + +#include "xo/indentlog/scope.hpp" +#include "xo/indentlog/print/tostr.hpp" +#include "xo/indentlog/print/tag.hpp" +#include +#include +#include +//#include + +namespace xo { + namespace gc { + /** @class CircularBuffer + * @brief A circular buffer + * + * push operations may overwrite prior contents, + * i.e. buffer behavior on overflow + * old + * + * @tparam T is type for buffer elements. + **/ + template + class CircularBuffer { + public: + using value_type = T; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + using reference = value_type &; + using const_reference = const value_type &; + using pointer = value_type *; + using const_pointer = const value_type *; + + template + class _iterator { + public: + using iterator_category = std::forward_iterator_tag; + using value_type = TT; + using difference_type = std::ptrdiff_t; + using pointer = value_type *; + using reference = value_type &; + + _iterator(Parent * p, std::int64_t ix) : parent_{p}, index_{ix} {} + + reference operator* () const { return (*parent_)[index_]; } + pointer operator->() const { return &(*parent_)[index_]; } + _iterator & operator++() { ++index_; return *this; } + _iterator operator++(int) { _iterator retval(parent_, index_); ++index_; return retval; } + + auto operator<=>(const _iterator & other) { + if (parent_ == other.parent_) + return index_ <=> other.index_; + else + return std::partial_ordering::unordered; + } + + bool operator==(const _iterator & other) const = default; + + private: + Parent * parent_ = nullptr; + /** index position + * (-1 = just before front = rend, 0 = front, z-1 = back, z = just after back = end) + **/ + std::int64_t index_ = 0; + }; + + using iterator = _iterator, T>; + using const_iterator = _iterator, const T>; + + public: + explicit CircularBuffer(std::size_t capacity = 0, bool debug_flag = false); + CircularBuffer(const CircularBuffer& other) = default; + CircularBuffer(CircularBuffer&& other) noexcept = default; + ~CircularBuffer() = default; + + static constexpr std::int64_t npos = -1; + + /** @return location of i'th element. i: 0=front, 1=second etc **/ + std::size_t location_of(std::size_t i) const; + /** @return ordinal index (relative to front) of location @p loc; + * npos if not used + **/ + //std::int64_t index_of(std::size_t loc) const; // not implemented yet + + // standard container methods + bool empty() const noexcept { return size_ == 0; } + size_type size() const noexcept { return size_; } + size_type max_size() const noexcept { return contents_.size(); } + // void reserve(size_type new_capacity); // not implemented + size_type capacity() const noexcept { return contents_.size(); } + // void shrink_to_fit(); // not implemented + + reference at(size_type pos) { + if ((pos < 0) || (pos >= size_)) { + throw std::out_of_range(tostr("CircularBuffer::at: index out of range", + xtag("pos", pos), xtag("size", size_))); + } + + return contents_[this->location_of(pos)]; + } + + const_reference at(size_type pos) const { + reference retval = const_cast(this)->at(pos); + return retval; + } + + reference operator[](size_type pos) { + return contents_[this->location_of(pos)]; + } + + const_reference operator[](size_type pos) const { + return contents_[this->location_of(pos)]; + } + + reference front() { return contents_[front_ix_]; } + const_reference front() const { + reference retval = const_cast(this)->front(); + return retval; + } + + reference back() { return contents_[location_of(size_ - 1)]; } + const_reference back() const { + reference retval = const_cast(this)->back(); + return retval; + } + + iterator begin() { return iterator(this, 0); } + iterator end() { return iterator(this, size_); } + const_iterator begin() const { return const_iterator(this, 0); } + const_iterator end() const { return const_iterator(this, size_); } + + // reverse_iterator rbegin(); + // reverse_iterator rend(); + // const_reverse_iterator rbegin() const; + // const_reverse_iterator rend() const; + + // General Methods + + void clear() { + size_ = 0; + front_ix_ = 0; + std::size_t capacity = contents_.size(); + contents_.clear(); + contents_.resize(capacity); + } + + /** push @p x on to the end of this buffer. + * If buffer is at capacity, overwrites the oldest element + **/ + CircularBuffer & push_back(const T & x); + + // template + //reference emplace_back(Args&&... args); + + CircularBuffer & pop_back(); + + // push_front(); + // pop_front(); + + CircularBuffer& operator=(const CircularBuffer& other) = default; + CircularBuffer& operator=(CircularBuffer&& other) noexcept = default; + + private: + /** number of elements in buffer. Not the same as @code contents_.size(); + * the latter represents buffer capacity. + * + * Promise: + * size_ <= contents_.size() + **/ + std::size_t size_ = 0; + /** first element is @code contents_.at(front_ix_) **/ + std::size_t front_ix_ = 0; + /** buffer contents. contents_.size() represents buffer capacity + * first element stored in @code contents_.at(front_) + * last element stored in @code contents_.at((front_ + size_ - 1) % contents_.size()) + **/ + std::vector contents_; + /** true to enable debug logging **/ + bool debug_flag_ = false; + }; + + template + CircularBuffer::CircularBuffer(std::size_t capacity, bool debug_flag) + : size_{0}, front_ix_{0}, contents_{capacity}, debug_flag_{debug_flag} + { + } + + template + std::size_t + CircularBuffer::location_of(std::size_t i) const + { + if (size_ == 0) + return 0; + else + return (front_ix_ + i) % size_; + } + + template + CircularBuffer & + CircularBuffer::push_back(const T & x) { + scope log(XO_DEBUG(debug_flag_), rtag("x", x), xrtag("size", size_)); + + if (size_ < contents_.size()) { + ++size_; + /* _after_ incr .size_ */ + std::size_t back_ix = location_of(size_ - 1); + + this->contents_[back_ix] = x; + + log && log(xtag("back_ix", back_ix), xtag("+size", size_)); + } else { + std::size_t back_ix = location_of(size_); + + this->contents_[back_ix] = x; + /* buffer was full, so oldest element replaced */ + this->front_ix_ = (this->front_ix_ + 1) % contents_.size(); + + log && log(xtag("back_ix", back_ix), xtag("+front", front_ix_)); + } + + return *this; + } + + template + CircularBuffer & + CircularBuffer::pop_back() { + if (size_ > 0) { + std::size_t back_ix = location_of(size_ - 1); + + this->contents_[back_ix] = T(); + --(this->size_); + } else { + assert(false); + } + + return *this; + } + } /*namespace gc*/ +} /*namespace xo*/ + +/* CircularBuffer.hpp */ diff --git a/utest/CircularBuffer.test.cpp b/utest/CircularBuffer.test.cpp new file mode 100644 index 00000000..f3d63c7e --- /dev/null +++ b/utest/CircularBuffer.test.cpp @@ -0,0 +1,174 @@ +/* CircularBuffer.test.cpp + * + * author: Roland Conybeare, Aug 2025 + */ + +#include "xo/alloc/CircularBuffer.hpp" +#include "xo/indentlog/print/vector.hpp" +#include + +namespace xo { + using xo::gc::CircularBuffer; + + namespace ut { + TEST_CASE("circular_buffer_0", "[circular_buffer]") + { + CircularBuffer q(10, false /*debug_flag*/); + q.push_back("a"); + REQUIRE(q.back() == "a"); + q.push_back("b"); + REQUIRE(q.back() == "b"); + q.push_back("c"); + REQUIRE(q.back() == "c"); + REQUIRE(q.location_of(0) == 0); + REQUIRE(q.location_of(1) == 1); + REQUIRE(q.location_of(2) == 2); + //REQUIRE(q.index_of(0) == 0); + + REQUIRE(q.size() == 3); + REQUIRE(q.front() == "a"); + REQUIRE(q.at(0) == "a"); + REQUIRE(q.at(1) == "b"); + REQUIRE(q.at(2) == "c"); + + CircularBuffer q2; + + q2 = q; + + q.clear(); + + REQUIRE(q2.size() == 3); + REQUIRE(q2.front() == "a"); + REQUIRE(q2.at(0) == "a"); + REQUIRE(q2.at(1) == "b"); + REQUIRE(q2.at(2) == "c"); + } + + TEST_CASE("circular_buffer_1", "[circular_buffer]") + { + CircularBuffer q(2, false /*debug_flag*/); + q.push_back("a"); + REQUIRE(q.back() == "a"); + q.push_back("b"); + REQUIRE(q.back() == "b"); + q.push_back("c"); + REQUIRE(q.back() == "c"); + REQUIRE(q.location_of(0) == 1); + REQUIRE(q.location_of(1) == 0); + //REQUIRE(q.index_of(0) == 0); + + REQUIRE(q.size() == 2); + REQUIRE(q.front() == "b"); + REQUIRE(q.at(0) == "b"); + REQUIRE(q.at(1) == "c"); + + { + std::size_t i = 0; + for (const auto & qi : q) { + REQUIRE(qi == q.at(i)); + ++i; + } + } + + CircularBuffer q2 = q; + + q.clear(); + + REQUIRE(q2.size() == 2); + REQUIRE(q2.front() == "b"); + REQUIRE(q2.at(0) == "b"); + REQUIRE(q2.at(1) == "c"); + + { + std::size_t i = 0; + for (const auto & qi : q) { + REQUIRE(qi == q2.at(i)); + ++i; + } + } + } + } + + namespace { + struct Testcase_CircularBuffer { + explicit Testcase_CircularBuffer(std::size_t capacity, + const std::vector & contents) + : capacity_{capacity}, + contents_{contents} {} + + std::size_t capacity_ = 0; + std::vector contents_; + }; + + std::vector + s_testcase_v = { + Testcase_CircularBuffer(0, {}), + Testcase_CircularBuffer(1, {"a"}), + Testcase_CircularBuffer(2, {"a", "b"}), + Testcase_CircularBuffer(2, {"a", "b", "c", "d"}) + }; + } + + namespace ut { + TEST_CASE("circular_buffer_2", "[circular_buffer]") + { + for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) { + const Testcase_CircularBuffer & tc = s_testcase_v[i_tc]; + + INFO(tostr(xtag("i_tc", i_tc), + xtag("capacity", tc.capacity_), + xrtag("contents", tc.contents_))); + + for (std::size_t j_phase = 0; j_phase < 2; ++j_phase) { + constexpr bool c_debug_flag = false; + + CircularBuffer q(tc.capacity_, false /*debug_flag*/); + + REQUIRE(q.empty()); + REQUIRE(q.size() == 0); + REQUIRE(q.begin() == q.end()); + REQUIRE(q.capacity() == tc.capacity_); + + std::size_t n = 0; + for (const auto & s : tc.contents_) { + INFO(tostr(xtag("n0", n), xtag("s", s))); + ++n; + INFO(xtag("n1", n)); + + q.push_back(s); + + REQUIRE(q.back() == s); + REQUIRE(q.capacity() == tc.capacity_); + REQUIRE(q.size() == std::min(n, tc.capacity_)); + + std::size_t i = 0; + for (const auto & qi : q) { + INFO(tostr(xtag("i", i), xtag("qi", qi))); + + if (n <= tc.capacity_) { + REQUIRE(qi == tc.contents_.at(i)); + REQUIRE(qi == tc.contents_[i]); + } else { + REQUIRE(qi == tc.contents_.at(n - tc.capacity_ + i)); + REQUIRE(qi == tc.contents_[n - tc.capacity_ + i]); + } + ++i; + } + + REQUIRE(i == std::min(n, tc.capacity_)); + + if (tc.contents_.size() <= tc.capacity_) + REQUIRE(q.front() == tc.contents_.at(0)); + } + + q.clear(); + + REQUIRE(q.size() == 0); + REQUIRE(q.capacity() == tc.capacity_); + } + } + } + } +} + +/* CircularBuffer.test.cpp */