From a78399099423a585453de2bdd760affd452e4707 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Tue, 25 Aug 2026 00:27:13 +0800 Subject: [PATCH 1/2] Replace boost circular_buffer usages --- src/stan/optimization/lbfgs_update.hpp | 8 +- src/stan/services/pathfinder/single.hpp | 6 +- src/stan/util/ring_buffer.hpp | 130 ++++++++++++++++++++++++ src/stan/variational/advi.hpp | 8 +- 4 files changed, 141 insertions(+), 11 deletions(-) create mode 100644 src/stan/util/ring_buffer.hpp diff --git a/src/stan/optimization/lbfgs_update.hpp b/src/stan/optimization/lbfgs_update.hpp index ba9d7afcb62..1f8719d4b12 100644 --- a/src/stan/optimization/lbfgs_update.hpp +++ b/src/stan/optimization/lbfgs_update.hpp @@ -2,7 +2,7 @@ #define STAN_OPTIMIZATION_LBFGS_UPDATE_HPP #include -#include +#include #include #include @@ -72,8 +72,8 @@ class LBFGSUpdate { **/ inline void search_direction(VectorT &pk, const VectorT &gk) const { std::vector alphas(_buf.size()); - typename boost::circular_buffer::const_reverse_iterator buf_rit; - typename boost::circular_buffer::const_iterator buf_it; + typename stan::util::ring_buffer::const_reverse_iterator buf_rit; + typename stan::util::ring_buffer::const_iterator buf_it; typename std::vector::const_iterator alpha_it; typename std::vector::reverse_iterator alpha_rit; @@ -103,7 +103,7 @@ class LBFGSUpdate { } protected: - boost::circular_buffer _buf; + stan::util::ring_buffer _buf; Scalar _gammak; }; } // namespace optimization diff --git a/src/stan/services/pathfinder/single.hpp b/src/stan/services/pathfinder/single.hpp index 7202fa6dcf2..a0df4efd0f0 100644 --- a/src/stan/services/pathfinder/single.hpp +++ b/src/stan/services/pathfinder/single.hpp @@ -12,7 +12,7 @@ #include #include #include -#include +#include #include #include #include @@ -659,8 +659,8 @@ inline auto pathfinder_lbfgs_single( + std::to_string(lbfgs.logp())); } int ret = 0; - boost::circular_buffer param_buff(max_history_size); - boost::circular_buffer grad_buff(max_history_size); + stan::util::ring_buffer param_buff(max_history_size); + stan::util::ring_buffer grad_buff(max_history_size); Eigen::VectorXd prev_params = Eigen::Map(cont_vector.data(), cont_vector.size()); std::size_t history_size = 0; diff --git a/src/stan/util/ring_buffer.hpp b/src/stan/util/ring_buffer.hpp new file mode 100644 index 00000000000..caf65a85830 --- /dev/null +++ b/src/stan/util/ring_buffer.hpp @@ -0,0 +1,130 @@ +#ifndef STAN_UTIL_RING_BUFFER_HPP +#define STAN_UTIL_RING_BUFFER_HPP + +#include +#include +#include +#include +#include +#include + +namespace stan { +namespace util { + +/** Fixed-capacity buffer that overwrites its oldest element when full. */ +template +class ring_buffer { + public: + explicit ring_buffer(size_t capacity) : buf_(capacity) { + if (capacity == 0) { + throw std::domain_error("ring_buffer capacity must be > 0"); + } + } + + size_t size() const { return size_; } + size_t capacity() const { return buf_.size(); } + + void clear() { + start_ = 0; + size_ = 0; + } + + void push_back() { + if (size_ < capacity()) { + ++size_; + } else { + start_ = (start_ + 1) % capacity(); + } + } + + template + void push_back(U&& value) { + push_back(); + back() = std::forward(value); + } + + T& back() { return (*this)[size_ - 1]; } + + T& operator[](size_t i) { return buf_[(start_ + i) % capacity()]; } + const T& operator[](size_t i) const { + return buf_[(start_ + i) % capacity()]; + } + + void rset_capacity(size_t new_capacity) { + if (new_capacity == 0) { + throw std::domain_error("ring_buffer capacity must be > 0"); + } + if (new_capacity == capacity()) { + return; + } + + std::vector new_buf(new_capacity); + size_t keep = std::min(size_, new_capacity); + for (size_t i = 0; i < keep; ++i) { + new_buf[i] = std::move((*this)[size_ - keep + i]); + } + buf_ = std::move(new_buf); + start_ = 0; + size_ = keep; + } + + class const_iterator { + public: + using iterator_category = std::bidirectional_iterator_tag; + using value_type = T; + using difference_type = std::ptrdiff_t; + using pointer = const T*; + using reference = const T&; + + const_iterator() = default; + const_iterator(const ring_buffer* buffer, size_t pos) + : buffer_(buffer), pos_(pos) {} + + reference operator*() const { return (*buffer_)[pos_]; } + + const_iterator& operator++() { + ++pos_; + return *this; + } + const_iterator operator++(int) { + const_iterator result = *this; + ++*this; + return result; + } + const_iterator& operator--() { + --pos_; + return *this; + } + + bool operator==(const const_iterator& other) const { + return buffer_ == other.buffer_ && pos_ == other.pos_; + } + bool operator!=(const const_iterator& other) const { + return !(*this == other); + } + + private: + const ring_buffer* buffer_ = nullptr; + size_t pos_ = 0; + }; + + const_iterator begin() const { return const_iterator(this, 0); } + const_iterator end() const { return const_iterator(this, size_); } + + using const_reverse_iterator = std::reverse_iterator; + const_reverse_iterator rbegin() const { + return const_reverse_iterator(end()); + } + const_reverse_iterator rend() const { + return const_reverse_iterator(begin()); + } + + private: + std::vector buf_; + size_t start_ = 0; + size_t size_ = 0; +}; + +} // namespace util +} // namespace stan +#endif diff --git a/src/stan/variational/advi.hpp b/src/stan/variational/advi.hpp index 681ce65e82c..579e222b41d 100644 --- a/src/stan/variational/advi.hpp +++ b/src/stan/variational/advi.hpp @@ -9,7 +9,7 @@ #include #include #include -#include +#include #include #include #include @@ -335,7 +335,7 @@ class advi { // Heuristic to estimate how far to look back in rolling window int cb_size = static_cast(std::max(0.1 * max_iterations / eval_elbo_, 2.0)); - boost::circular_buffer elbo_diff(cb_size); + stan::util::ring_buffer elbo_diff(cb_size); logger.info("Begin stochastic gradient ascent."); logger.info( @@ -528,10 +528,10 @@ class advi { * @param[in] cb circular buffer with some number of values in it. * @return median of values in circular buffer. */ - double circ_buff_median(const boost::circular_buffer& cb) const { + double circ_buff_median(const stan::util::ring_buffer& cb) const { // FIXME: naive implementation; creates a copy as a vector std::vector v; - for (boost::circular_buffer::const_iterator i = cb.begin(); + for (stan::util::ring_buffer::const_iterator i = cb.begin(); i != cb.end(); ++i) { v.push_back(*i); } From 256ce6f63cea9a1678c6b8085ba498119766d106 Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Tue, 15 Sep 2026 17:39:57 -0400 Subject: [PATCH 2/2] Align ring_buffer with standard container conventions Give ring_buffer the member typedefs, iterators, element access and move semantics callers expect from a standard sequence container. - add value_type, size_type, difference_type, reference, const_reference, pointer and const_pointer, plus the four iterator typedefs - replace the const-only bidirectional iterator with a single iter_impl providing mutable and constant random access iterators, one-way iterator to const_iterator conversion, operator-> and post-decrement, along with cbegin/cend/crbegin/crend - reference qualify operator[], front() and back() with &, const& and && overloads, and add bounds checked at() and empty() - define move construction and assignment so the source is left empty rather than reporting a stale size over an emptied container, which divided by zero on any subsequent access - add swap and the six comparison operators over the logical contents - replace the wrapping modulo with a conditional subtraction, keeping integer division out of the L-BFGS and pathfinder inner loops - make element access and iterator dereference conditionally noexcept on the backing container's subscript and size operations - build the replacement buffer in reset_capacity as a Container rather than unconditionally as a std::vector Fix the call in LBFGSUpdate::set_history_size, which still used the boost circular_buffer spelling rset_capacity. Add unit tests covering the iterator requirements, the reference qualified accessors, moved-from state, comparisons and the conditional exception specifications. --- src/stan/optimization/lbfgs_update.hpp | 2 +- src/stan/util/ring_buffer.hpp | 425 +++++++++++++++++---- src/test/unit/util/ring_buffer_test.cpp | 475 ++++++++++++++++++++++++ 3 files changed, 832 insertions(+), 70 deletions(-) create mode 100644 src/test/unit/util/ring_buffer_test.cpp diff --git a/src/stan/optimization/lbfgs_update.hpp b/src/stan/optimization/lbfgs_update.hpp index 1f8719d4b12..303a852f164 100644 --- a/src/stan/optimization/lbfgs_update.hpp +++ b/src/stan/optimization/lbfgs_update.hpp @@ -28,7 +28,7 @@ class LBFGSUpdate { * * @param L New size of buffer. **/ - void set_history_size(size_t L) { _buf.rset_capacity(L); } + void set_history_size(size_t L) { _buf.reset_capacity(L); } /** * Add a new set of update vectors to the history. diff --git a/src/stan/util/ring_buffer.hpp b/src/stan/util/ring_buffer.hpp index caf65a85830..decf8d8a094 100644 --- a/src/stan/util/ring_buffer.hpp +++ b/src/stan/util/ring_buffer.hpp @@ -4,53 +4,369 @@ #include #include #include +#include #include +#include #include #include namespace stan { namespace util { -/** Fixed-capacity buffer that overwrites its oldest element when full. */ -template +/** + * Fixed-capacity buffer that overwrites its oldest element when full. + * + * The interface follows the standard sequence container conventions where + * they make sense for a ring: member typedefs, mutable and constant + * random access iterators, reference qualified element access, and + * conditionally `noexcept` operations that track the backing container. + * + * `data()` is deliberately absent because the elements are not contiguous + * in logical order, and `allocator_type` is absent so that containers + * without allocators may be used as the backing store. + * + * @tparam T Type of elements stored in the buffer + * @tparam Container Type of container used to store the elements + **/ +template >> class ring_buffer { + /** True if the container's non-const subscript cannot throw. */ + static constexpr bool nothrow_subscript_ + = noexcept(std::declval()[std::declval()]); + /** True if the container's const subscript cannot throw. */ + static constexpr bool nothrow_const_subscript_ + = noexcept(std::declval()[std::declval()]); + /** True if querying the container's size cannot throw. */ + static constexpr bool nothrow_size_ + = noexcept(std::declval().size()); + /** + * True if indexing cannot throw. Indexing consults the capacity to wrap, + * so it is only nothrow when the container's size query is too. + **/ + static constexpr bool nothrow_index_ = nothrow_subscript_ && nothrow_size_; + /** True if const indexing cannot throw. */ + static constexpr bool nothrow_const_index_ + = nothrow_const_subscript_ && nothrow_size_; + public: - explicit ring_buffer(size_t capacity) : buf_(capacity) { + using value_type = T; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + using reference = T&; + using const_reference = const T&; + using pointer = T*; + using const_pointer = const T*; + + /** + * Random access iterator over the buffer's logical element order. + * + * @tparam Const True for the constant iterator + **/ + template + class iter_impl { + template + friend class iter_impl; + friend class ring_buffer; + + using buffer_ptr + = std::conditional_t; + using owner_ref + = std::conditional_t; + + /** True if dereferencing cannot throw, tracking the buffer's subscript. */ + static constexpr bool nothrow_deref_ + = noexcept(std::declval()[std::declval()]); + + public: + using iterator_category = std::random_access_iterator_tag; + using value_type = T; + using difference_type = std::ptrdiff_t; + using pointer = std::conditional_t; + using reference = std::conditional_t; + + iter_impl() = default; + iter_impl(buffer_ptr buffer, size_type pos) noexcept + : buffer_(buffer), pos_(pos) {} + + /** Converts a mutable iterator to a constant iterator. */ + template > + iter_impl(const iter_impl& other) noexcept // NOLINT + : buffer_(other.buffer_), pos_(other.pos_) {} + + reference operator*() const noexcept(nothrow_deref_) { + return (*buffer_)[pos_]; + } + pointer operator->() const noexcept(nothrow_deref_) { + return std::addressof((*buffer_)[pos_]); + } + reference operator[](difference_type n) const noexcept(nothrow_deref_) { + return (*buffer_)[pos_ + n]; + } + + iter_impl& operator++() noexcept { + ++pos_; + return *this; + } + iter_impl operator++(int) noexcept { + iter_impl result = *this; + ++pos_; + return result; + } + iter_impl& operator--() noexcept { + --pos_; + return *this; + } + iter_impl operator--(int) noexcept { + iter_impl result = *this; + --pos_; + return result; + } + + iter_impl& operator+=(difference_type n) noexcept { + pos_ += n; + return *this; + } + iter_impl& operator-=(difference_type n) noexcept { + pos_ -= n; + return *this; + } + friend iter_impl operator+(iter_impl it, difference_type n) noexcept { + it += n; + return it; + } + friend iter_impl operator+(difference_type n, iter_impl it) noexcept { + it += n; + return it; + } + friend iter_impl operator-(iter_impl it, difference_type n) noexcept { + it -= n; + return it; + } + friend difference_type operator-(const iter_impl& a, + const iter_impl& b) noexcept { + return static_cast(a.pos_) + - static_cast(b.pos_); + } + + friend bool operator==(const iter_impl& a, const iter_impl& b) noexcept { + return a.buffer_ == b.buffer_ && a.pos_ == b.pos_; + } + friend bool operator!=(const iter_impl& a, const iter_impl& b) noexcept { + return !(a == b); + } + friend bool operator<(const iter_impl& a, const iter_impl& b) noexcept { + return a.pos_ < b.pos_; + } + friend bool operator>(const iter_impl& a, const iter_impl& b) noexcept { + return b.pos_ < a.pos_; + } + friend bool operator<=(const iter_impl& a, const iter_impl& b) noexcept { + return !(b.pos_ < a.pos_); + } + friend bool operator>=(const iter_impl& a, const iter_impl& b) noexcept { + return !(a.pos_ < b.pos_); + } + + private: + buffer_ptr buffer_ = nullptr; + size_type pos_ = 0; + }; + + using iterator = iter_impl; + using const_iterator = iter_impl; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + + /** + * Construct a buffer holding up to `capacity` elements. + * + * @param[in] capacity Maximum number of elements, must be positive + * @throw std::domain_error if `capacity` is zero + **/ + explicit ring_buffer(size_type capacity) : buf_(capacity) { if (capacity == 0) { throw std::domain_error("ring_buffer capacity must be > 0"); } } - size_t size() const { return size_; } - size_t capacity() const { return buf_.size(); } + /** Construct an empty buffer with room for a single element. */ + ring_buffer() : buf_(1) {} + + ring_buffer(const ring_buffer&) = default; + ring_buffer& operator=(const ring_buffer&) = default; + + /** + * Move construct, leaving the source empty and safe to reuse. + * + * @param[in,out] other Buffer to move from + **/ + ring_buffer(ring_buffer&& other) noexcept( + std::is_nothrow_move_constructible::value) + : buf_(std::move(other.buf_)), start_(other.start_), size_(other.size_) { + other.start_ = 0; + other.size_ = 0; + } + + /** + * Move assign, leaving the source empty and safe to reuse. + * + * @param[in,out] other Buffer to move from + * @return reference to this buffer + **/ + ring_buffer& operator=(ring_buffer&& other) noexcept( + std::is_nothrow_move_assignable::value) { + if (this != &other) { + buf_ = std::move(other.buf_); + start_ = other.start_; + size_ = other.size_; + other.start_ = 0; + other.size_ = 0; + } + return *this; + } + + inline size_type size() const noexcept { return size_; } + inline size_type capacity() const noexcept(nothrow_size_) { + return buf_.size(); + } + inline bool empty() const noexcept { return size_ == 0; } - void clear() { + inline void clear() noexcept { start_ = 0; size_ = 0; } - void push_back() { - if (size_ < capacity()) { + /** Advance the buffer by one element without assigning to it. */ + inline void push_back() noexcept(nothrow_size_) { + const size_type cap = capacity(); + if (size_ < cap) { ++size_; - } else { - start_ = (start_ + 1) % capacity(); + } else if (cap > 0) { + start_ = (start_ + 1 == cap) ? 0 : start_ + 1; } } + /** + * Append a value, overwriting the oldest element when full. + * + * @tparam U Type of the value, deduced + * @param[in] value Value to append + **/ template void push_back(U&& value) { push_back(); back() = std::forward(value); } - T& back() { return (*this)[size_ - 1]; } + reference front() & noexcept(nothrow_index_) { return (*this)[0]; } + const_reference front() const& noexcept(nothrow_const_index_) { + return (*this)[0]; + } + T&& front() && noexcept(nothrow_index_) { return std::move((*this)[0]); } + + reference back() & noexcept(nothrow_index_) { return (*this)[size_ - 1]; } + const_reference back() const& noexcept(nothrow_const_index_) { + return (*this)[size_ - 1]; + } + T&& back() && noexcept(nothrow_index_) { + return std::move((*this)[size_ - 1]); + } + + reference operator[](size_type i) & noexcept(nothrow_index_) { + return buf_[offset(i)]; + } + const_reference operator[](size_type i) const& noexcept( + nothrow_const_index_) { + return buf_[offset(i)]; + } + T&& operator[](size_type i) && noexcept(nothrow_index_) { + return std::move(buf_[offset(i)]); + } - T& operator[](size_t i) { return buf_[(start_ + i) % capacity()]; } - const T& operator[](size_t i) const { - return buf_[(start_ + i) % capacity()]; + /** + * Return the element at index `i` with bounds checking. + * + * @param[in] i Index into the buffer's logical order + * @return reference to the element + * @throw std::out_of_range if `i` is not less than `size()` + **/ + reference at(size_type i) & { + check_index(i); + return buf_[offset(i)]; + } + const_reference at(size_type i) const& { + check_index(i); + return buf_[offset(i)]; + } + + iterator begin() noexcept { return iterator(this, 0); } + const_iterator begin() const noexcept { return const_iterator(this, 0); } + const_iterator cbegin() const noexcept { return const_iterator(this, 0); } + + iterator end() noexcept { return iterator(this, size_); } + const_iterator end() const noexcept { return const_iterator(this, size_); } + const_iterator cend() const noexcept { return const_iterator(this, size_); } + + reverse_iterator rbegin() noexcept { return reverse_iterator(end()); } + const_reverse_iterator rbegin() const noexcept { + return const_reverse_iterator(end()); + } + const_reverse_iterator crbegin() const noexcept { + return const_reverse_iterator(cend()); + } + + reverse_iterator rend() noexcept { return reverse_iterator(begin()); } + const_reverse_iterator rend() const noexcept { + return const_reverse_iterator(begin()); + } + const_reverse_iterator crend() const noexcept { + return const_reverse_iterator(cbegin()); + } + + /** + * Exchange contents with another buffer. + * + * @param[in,out] other Buffer to swap with + **/ + void swap(ring_buffer& other) noexcept( + std::is_nothrow_swappable::value) { + using std::swap; + swap(buf_, other.buf_); + swap(start_, other.start_); + swap(size_, other.size_); + } + + friend void swap(ring_buffer& a, + ring_buffer& b) noexcept(noexcept(a.swap(b))) { + a.swap(b); + } + + friend bool operator==(const ring_buffer& a, const ring_buffer& b) { + return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin()); + } + friend bool operator!=(const ring_buffer& a, const ring_buffer& b) { + return !(a == b); + } + friend bool operator<(const ring_buffer& a, const ring_buffer& b) { + return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end()); + } + friend bool operator>(const ring_buffer& a, const ring_buffer& b) { + return b < a; + } + friend bool operator<=(const ring_buffer& a, const ring_buffer& b) { + return !(b < a); + } + friend bool operator>=(const ring_buffer& a, const ring_buffer& b) { + return !(a < b); } - void rset_capacity(size_t new_capacity) { + /** + * Resize the buffer, keeping the most recently added elements. + * + * @param[in] new_capacity Maximum number of elements, must be positive + * @throw std::domain_error if `new_capacity` is zero + **/ + void reset_capacity(size_type new_capacity) { if (new_capacity == 0) { throw std::domain_error("ring_buffer capacity must be > 0"); } @@ -58,9 +374,9 @@ class ring_buffer { return; } - std::vector new_buf(new_capacity); - size_t keep = std::min(size_, new_capacity); - for (size_t i = 0; i < keep; ++i) { + Container new_buf(new_capacity); + size_type keep = std::min(size_, new_capacity); + for (size_type i = 0; i < keep; ++i) { new_buf[i] = std::move((*this)[size_ - keep + i]); } buf_ = std::move(new_buf); @@ -68,61 +384,32 @@ class ring_buffer { size_ = keep; } - class const_iterator { - public: - using iterator_category = std::bidirectional_iterator_tag; - using value_type = T; - using difference_type = std::ptrdiff_t; - using pointer = const T*; - using reference = const T&; - - const_iterator() = default; - const_iterator(const ring_buffer* buffer, size_t pos) - : buffer_(buffer), pos_(pos) {} - - reference operator*() const { return (*buffer_)[pos_]; } - - const_iterator& operator++() { - ++pos_; - return *this; - } - const_iterator operator++(int) { - const_iterator result = *this; - ++*this; - return result; - } - const_iterator& operator--() { - --pos_; - return *this; - } + private: + /** + * Map a logical index onto the backing container's index. + * + * A conditional subtraction replaces a modulo, which keeps integer + * division out of the hot path and leaves a zero-capacity buffer with + * ordinary out-of-contract behavior rather than a division by zero. + * + * @param[in] i Index into the buffer's logical order + * @return index into the backing container + **/ + inline size_type offset(size_type i) const noexcept(nothrow_size_) { + const size_type cap = capacity(); + const size_type j = start_ + i; + return j >= cap ? j - cap : j; + } - bool operator==(const const_iterator& other) const { - return buffer_ == other.buffer_ && pos_ == other.pos_; - } - bool operator!=(const const_iterator& other) const { - return !(*this == other); + inline void check_index(size_type i) const { + if (i >= size_) { + throw std::out_of_range("ring_buffer index out of range"); } - - private: - const ring_buffer* buffer_ = nullptr; - size_t pos_ = 0; - }; - - const_iterator begin() const { return const_iterator(this, 0); } - const_iterator end() const { return const_iterator(this, size_); } - - using const_reverse_iterator = std::reverse_iterator; - const_reverse_iterator rbegin() const { - return const_reverse_iterator(end()); - } - const_reverse_iterator rend() const { - return const_reverse_iterator(begin()); } - private: - std::vector buf_; - size_t start_ = 0; - size_t size_ = 0; + Container buf_; + size_type start_ = 0; + size_type size_ = 0; }; } // namespace util diff --git a/src/test/unit/util/ring_buffer_test.cpp b/src/test/unit/util/ring_buffer_test.cpp new file mode 100644 index 00000000000..2ce96e1c423 --- /dev/null +++ b/src/test/unit/util/ring_buffer_test.cpp @@ -0,0 +1,475 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using stan::util::ring_buffer; + +namespace { + +/** + * Element type that records how many times it has been moved, so tests can + * prove that the rvalue-qualified accessors actually move rather than copy. + */ +struct tracked { + int value = 0; + int moves = 0; + tracked() = default; + explicit tracked(int v) : value(v) {} + tracked(const tracked&) = default; + tracked& operator=(const tracked&) = default; + tracked(tracked&& other) noexcept + : value(other.value), moves(other.moves + 1) { + other.value = -1; + } + tracked& operator=(tracked&& other) noexcept { + value = other.value; + moves = other.moves + 1; + other.value = -1; + return *this; + } +}; + +/** + * Backing store whose subscript operators are *not* noexcept, used to check + * that ring_buffer's exception specifications track the container's. + */ +template +struct checked_vector : std::vector { + using std::vector::vector; + T& operator[](std::size_t i) { return this->at(i); } + const T& operator[](std::size_t i) const { return this->at(i); } +}; + +/** + * Backing store whose size() may throw, used to check that accessors which + * consult the capacity report that in their exception specification. + */ +template +struct loud_size_vector : std::vector { + using std::vector::vector; + // deliberately not noexcept, unlike std::vector::size + std::size_t size() const { return std::vector::size(); } +}; + +struct point { + int x = 0; + int y = 0; +}; + +ring_buffer filled(std::size_t capacity, std::size_t n) { + ring_buffer b(capacity); + for (std::size_t i = 0; i < n; ++i) { + b.push_back(static_cast(i)); + } + return b; +} + +std::vector to_vector(const ring_buffer& b) { + return std::vector(b.begin(), b.end()); +} + +} // namespace + +// ---------------------------------------------------------------- typedefs + +TEST(RingBuffer, exposes_standard_member_typedefs) { + using rb = ring_buffer; + static_assert(std::is_same::value, "value_type"); + static_assert(std::is_same::value, "size_type"); + static_assert(std::is_same::value, + "difference_type"); + static_assert(std::is_same::value, "reference"); + static_assert(std::is_same::value, + "const_reference"); + static_assert(std::is_same::value, "pointer"); + static_assert(std::is_same::value, + "const_pointer"); + SUCCEED(); +} + +// --------------------------------------------------------------- iterators + +TEST(RingBuffer, iterators_are_random_access) { + using rb = ring_buffer; + static_assert( + std::is_same::iterator_category, + std::random_access_iterator_tag>::value, + "iterator must be random access"); + static_assert( + std::is_same::iterator_category, + std::random_access_iterator_tag>::value, + "const_iterator must be random access"); + SUCCEED(); +} + +TEST(RingBuffer, iterator_converts_to_const_iterator_but_not_back) { + using rb = ring_buffer; + static_assert(std::is_convertible::value, + "iterator -> const_iterator"); + static_assert(!std::is_convertible::value, + "const_iterator must not convert to iterator"); + SUCCEED(); +} + +TEST(RingBuffer, begin_on_const_buffer_yields_const_iterator) { + using rb = ring_buffer; + static_assert( + std::is_same().begin()), rb::iterator>::value, + "non-const begin"); + static_assert(std::is_same().begin()), + rb::const_iterator>::value, + "const begin"); + static_assert(std::is_same().cbegin()), + rb::const_iterator>::value, + "cbegin"); + static_assert(std::is_same().crbegin()), + rb::const_reverse_iterator>::value, + "crbegin"); + SUCCEED(); +} + +TEST(RingBuffer, mutable_iterator_can_modify_elements) { + ring_buffer b = filled(4, 4); + for (auto it = b.begin(); it != b.end(); ++it) { + *it *= 10; + } + EXPECT_EQ(std::vector({0, 10, 20, 30}), to_vector(b)); +} + +TEST(RingBuffer, iterator_supports_random_access_arithmetic) { + ring_buffer b = filled(5, 5); + auto first = b.begin(); + auto last = b.end(); + EXPECT_EQ(5, last - first); + EXPECT_EQ(5, std::distance(first, last)); + EXPECT_EQ(2, *(first + 2)); + EXPECT_EQ(2, first[2]); + EXPECT_EQ(4, *(last - 1)); + EXPECT_TRUE(first < last); + EXPECT_TRUE(last > first); + EXPECT_TRUE(first <= first); + EXPECT_TRUE(last >= last); + auto mid = first; + mid += 3; + EXPECT_EQ(3, *mid); + mid -= 2; + EXPECT_EQ(1, *mid); +} + +TEST(RingBuffer, iterator_supports_arrow) { + ring_buffer b(2); + b.push_back(point{1, 2}); + EXPECT_EQ(1, b.begin()->x); + EXPECT_EQ(2, b.begin()->y); +} + +TEST(RingBuffer, iteration_follows_logical_order_after_wrapping) { + ring_buffer b = filled(3, 5); // 0,1 overwritten + EXPECT_EQ(3u, b.size()); + EXPECT_EQ(std::vector({2, 3, 4}), to_vector(b)); + EXPECT_EQ(2, b[0]); + EXPECT_EQ(4, b[2]); +} + +TEST(RingBuffer, reverse_iteration_visits_newest_first) { + ring_buffer b = filled(3, 5); + std::vector seen(b.rbegin(), b.rend()); + EXPECT_EQ(std::vector({4, 3, 2}), seen); + std::vector cseen(b.crbegin(), b.crend()); + EXPECT_EQ(std::vector({4, 3, 2}), cseen); +} + +TEST(RingBuffer, works_with_standard_algorithms) { + ring_buffer b = filled(4, 4); + EXPECT_EQ(6, std::accumulate(b.begin(), b.end(), 0)); + EXPECT_TRUE(std::is_sorted(b.begin(), b.end())); +} + +// --------------------------------------------------------- element access + +TEST(RingBuffer, element_access_is_reference_qualified) { + using rb = ring_buffer; + static_assert(std::is_same()[0]), int&>::value, + "lvalue subscript"); + static_assert( + std::is_same()[0]), const int&>::value, + "const lvalue subscript"); + static_assert(std::is_same()[0]), int&&>::value, + "rvalue subscript"); + static_assert(std::is_same().back()), int&>::value, + "lvalue back"); + static_assert(std::is_same().back()), + const int&>::value, + "const back"); + static_assert( + std::is_same().back()), int&&>::value, + "rvalue back"); + static_assert( + std::is_same().front()), int&&>::value, + "rvalue front"); + SUCCEED(); +} + +TEST(RingBuffer, rvalue_subscript_moves_out_of_the_element) { + ring_buffer b(2); + b.push_back(tracked(7)); + const int moves_before = b[0].moves; + tracked taken = std::move(b)[0]; + EXPECT_EQ(7, taken.value); + EXPECT_GT(taken.moves, moves_before); +} + +TEST(RingBuffer, rvalue_back_moves_out_of_the_element) { + ring_buffer b(2); + b.push_back(tracked(9)); + tracked taken = std::move(b).back(); + EXPECT_EQ(9, taken.value); + EXPECT_GT(taken.moves, 0); +} + +TEST(RingBuffer, front_and_back_track_the_logical_ends) { + ring_buffer b = filled(3, 5); + EXPECT_EQ(2, b.front()); + EXPECT_EQ(4, b.back()); + const ring_buffer& cb = b; + EXPECT_EQ(2, cb.front()); + EXPECT_EQ(4, cb.back()); +} + +TEST(RingBuffer, empty_reports_logical_emptiness) { + ring_buffer b(4); + EXPECT_TRUE(b.empty()); + b.push_back(1); + EXPECT_FALSE(b.empty()); + b.clear(); + EXPECT_TRUE(b.empty()); +} + +TEST(RingBuffer, at_throws_out_of_range_past_the_end) { + ring_buffer b = filled(4, 2); + EXPECT_EQ(1, b.at(1)); + EXPECT_THROW(b.at(2), std::out_of_range); + const ring_buffer& cb = b; + EXPECT_THROW(cb.at(2), std::out_of_range); +} + +// -------------------------------------------------------- move semantics + +TEST(RingBuffer, move_construction_leaves_source_empty_and_usable) { + ring_buffer a = filled(4, 3); + ring_buffer b(std::move(a)); + EXPECT_EQ(std::vector({0, 1, 2}), to_vector(b)); + EXPECT_EQ(0u, a.size()); + EXPECT_TRUE(a.empty()); + EXPECT_EQ(a.begin(), a.end()); + EXPECT_EQ(0, std::distance(a.begin(), a.end())); +} + +TEST(RingBuffer, move_assignment_leaves_source_empty_and_usable) { + ring_buffer a = filled(4, 3); + ring_buffer b(2); + b = std::move(a); + EXPECT_EQ(std::vector({0, 1, 2}), to_vector(b)); + EXPECT_EQ(0u, a.size()); + EXPECT_TRUE(a.empty()); + EXPECT_EQ(a.begin(), a.end()); +} + +TEST(RingBuffer, moved_from_buffer_can_be_refilled) { + ring_buffer a = filled(4, 3); + ring_buffer b(std::move(a)); + a.reset_capacity(2); + a.push_back(42); + EXPECT_EQ(1u, a.size()); + EXPECT_EQ(42, a.back()); +} + +TEST(RingBuffer, move_operations_are_noexcept) { + using rb = ring_buffer; + static_assert(std::is_nothrow_move_constructible::value, + "move ctor noexcept"); + static_assert(std::is_nothrow_move_assignable::value, + "move assign noexcept"); + SUCCEED(); +} + +TEST(RingBuffer, copy_construction_is_independent) { + ring_buffer a = filled(4, 3); + ring_buffer b(a); + b[0] = 99; + EXPECT_EQ(0, a[0]); + EXPECT_EQ(99, b[0]); + EXPECT_EQ(3u, a.size()); +} + +// ------------------------------------------------------ swap / comparison + +TEST(RingBuffer, swap_exchanges_contents) { + ring_buffer a = filled(4, 3); + ring_buffer b = filled(2, 2); + a.swap(b); + EXPECT_EQ(std::vector({0, 1}), to_vector(a)); + EXPECT_EQ(std::vector({0, 1, 2}), to_vector(b)); +} + +TEST(RingBuffer, free_swap_is_found_by_adl) { + ring_buffer a = filled(4, 3); + ring_buffer b = filled(2, 2); + using std::swap; + swap(a, b); + EXPECT_EQ(2u, a.size()); + EXPECT_EQ(3u, b.size()); +} + +TEST(RingBuffer, equality_compares_logical_contents_not_capacity) { + ring_buffer a(3); + ring_buffer b(8); + a.push_back(1); + a.push_back(2); + b.push_back(1); + b.push_back(2); + EXPECT_TRUE(a == b); + EXPECT_FALSE(a != b); + b.push_back(3); + EXPECT_TRUE(a != b); +} + +TEST(RingBuffer, equality_ignores_overwritten_elements) { + ring_buffer a = filled(2, 5); // holds 3,4 + ring_buffer b(2); + b.push_back(3); + b.push_back(4); + EXPECT_TRUE(a == b); +} + +TEST(RingBuffer, relational_operators_are_lexicographical) { + ring_buffer a(4); + ring_buffer b(4); + a.push_back(1); + a.push_back(2); + b.push_back(1); + b.push_back(3); + EXPECT_TRUE(a < b); + EXPECT_TRUE(a <= b); + EXPECT_TRUE(b > a); + EXPECT_TRUE(b >= a); + EXPECT_FALSE(b < a); +} + +// ----------------------------------------------------- conditional noexcept + +TEST(RingBuffer, subscript_noexcept_tracks_the_container) { + // Asserted as a *relationship*, not a hardcoded true: whether + // std::vector::operator[] is noexcept is implementation defined. + using vec_rb = ring_buffer; + constexpr bool vec_ok + = noexcept(std::declval&>()[std::size_t{0}]); + static_assert(noexcept(std::declval()[0]) == vec_ok, + "vector-backed subscript must match the container"); + static_assert(noexcept(*std::declval()) == vec_ok, + "iterator deref must match the container"); + + using checked_rb = ring_buffer>; + static_assert(!noexcept(std::declval()[0]), + "throwing container must make subscript throwing"); + static_assert(!noexcept(*std::declval()), + "throwing container must make iterator deref throwing"); + static_assert(!noexcept(std::declval().back()), + "throwing container must make back() throwing"); + SUCCEED(); +} + +TEST(RingBuffer, iterator_navigation_is_unconditionally_noexcept) { + using checked_rb = ring_buffer>; + static_assert(noexcept(++std::declval()), + "increment never touches the container"); + static_assert(noexcept(std::declval() + == std::declval()), + "comparison never touches the container"); + static_assert(noexcept(std::declval().begin()), + "begin never touches the container"); + SUCCEED(); +} + +TEST(RingBuffer, subscript_noexcept_accounts_for_the_container_size_call) { + // Indexing consults capacity() to wrap, so a container whose size() can + // throw must not yield a noexcept subscript. + using rb = ring_buffer>; + static_assert(!noexcept(std::declval().capacity()), + "capacity must follow the container's size()"); + static_assert(!noexcept(std::declval()[0]), + "subscript consults capacity, so it cannot claim noexcept"); + static_assert(!noexcept(std::declval()[0]), + "const subscript consults capacity"); + static_assert(!noexcept(std::declval().back()), + "back consults capacity"); + static_assert(!noexcept(*std::declval()), + "iterator deref consults capacity"); + SUCCEED(); +} + +// ------------------------------------------------------------- capacity + +TEST(RingBuffer, default_constructed_buffer_has_capacity_one) { + ring_buffer b; + EXPECT_EQ(1u, b.capacity()); + EXPECT_EQ(0u, b.size()); + EXPECT_TRUE(b.empty()); + b.push_back(5); + EXPECT_EQ(1u, b.size()); + EXPECT_EQ(5, b.back()); +} + +TEST(RingBuffer, constructing_with_zero_capacity_throws) { + EXPECT_THROW(ring_buffer(0), std::domain_error); +} + +TEST(RingBuffer, reset_capacity_to_zero_throws) { + ring_buffer b(4); + EXPECT_THROW(b.reset_capacity(0), std::domain_error); +} + +TEST(RingBuffer, reset_capacity_shrink_keeps_newest_elements) { + ring_buffer b = filled(5, 5); + b.reset_capacity(2); + EXPECT_EQ(2u, b.capacity()); + EXPECT_EQ(std::vector({3, 4}), to_vector(b)); +} + +TEST(RingBuffer, reset_capacity_grow_keeps_all_elements) { + ring_buffer b = filled(3, 5); + b.reset_capacity(6); + EXPECT_EQ(6u, b.capacity()); + EXPECT_EQ(std::vector({2, 3, 4}), to_vector(b)); + b.push_back(5); + EXPECT_EQ(std::vector({2, 3, 4, 5}), to_vector(b)); +} + +TEST(RingBuffer, reset_capacity_uses_the_container_type) { + // Regression: reset_capacity used to build a std::vector unconditionally, + // which fails to compile for any other backing store. + ring_buffer> b(3); + b.push_back(1); + b.push_back(2); + b.reset_capacity(4); + EXPECT_EQ(4u, b.capacity()); + EXPECT_EQ(2u, b.size()); + EXPECT_EQ(1, b[0]); + EXPECT_EQ(2, b[1]); +} + +TEST(RingBuffer, clear_resets_size_but_not_capacity) { + ring_buffer b = filled(4, 4); + b.clear(); + EXPECT_EQ(0u, b.size()); + EXPECT_EQ(4u, b.capacity()); + EXPECT_EQ(b.begin(), b.end()); +}