diff --git a/doc/modules/ROOT/pages/5.buffers/5c.sequences.adoc b/doc/modules/ROOT/pages/5.buffers/5c.sequences.adoc index b2bdb58c9..e4101711b 100644 --- a/doc/modules/ROOT/pages/5.buffers/5c.sequences.adoc +++ b/doc/modules/ROOT/pages/5.buffers/5c.sequences.adoc @@ -100,45 +100,47 @@ void process(Buffers const& bufs) These functions handle both single buffers (returning pointer-to-self) and ranges (returning standard iterators). -== consuming_buffers +== buffer_slice -When transferring data incrementally, `consuming_buffers` tracks progress: +When transferring data incrementally, `buffer_slice` returns a slice that tracks progress: [source,cpp] ---- -#include +#include template task read_all(Stream& stream, Buffers buffers) { - consuming_buffers remaining(buffers); + auto remaining = buffer_slice(buffers); + std::size_t const total_size = buffer_size(buffers); std::size_t total = 0; - - while (buffer_size(remaining) > 0) + + while (total < total_size) { - auto [ec, n] = co_await stream.read_some(remaining); - remaining.consume(n); + auto [ec, n] = co_await stream.read_some(remaining.data()); + remaining.remove_prefix(n); total += n; if (ec) break; } - + co_return total; } ---- -`consuming_buffers` wraps a buffer sequence and provides: +`buffer_slice(seq, offset, length)` returns an object of unspecified type that satisfies the `Slice` concept, providing: -* `consume(n)` — Mark `n` bytes as consumed (remove from front) -* Iteration over unconsumed buffers -* `buffer_size()` of remaining bytes +* `data()` — Buffer sequence view of the slice's current bytes (pass to `read_some`/`write_some`) +* `remove_prefix(n)` — Advance the start by `n` bytes + +The `offset` and `length` parameters (both optional) make `buffer_slice` a general byte sub-range primitive, not just an iteration-state holder. == Why Bidirectional? The concepts require bidirectional ranges (not just forward ranges) for two reasons: 1. Some algorithms traverse buffers backwards -2. `consuming_buffers` needs to adjust the first buffer's start position +2. The buffer sequence view returned by `Slice::data()` needs to adjust the first and last buffers' bounds If your custom buffer sequence only provides forward iteration, wrap it in a type that provides bidirectional access. @@ -151,8 +153,11 @@ If your custom buffer sequence only provides forward iteration, wrap it in a typ | `` | Concepts and iteration functions -| `` -| Incremental consumption wrapper +| `` +| Byte sub-range slicing algorithm + +| `` +| `Slice` concept |=== You have now learned how buffer sequences enable zero-allocation composition. Continue to xref:5.buffers/5d.system-io.adoc[System I/O Integration] to see how buffer sequences interface with operating system I/O. diff --git a/doc/modules/ROOT/pages/5.buffers/5e.algorithms.adoc b/doc/modules/ROOT/pages/5.buffers/5e.algorithms.adoc index b10f8c3b4..ca169e594 100644 --- a/doc/modules/ROOT/pages/5.buffers/5e.algorithms.adoc +++ b/doc/modules/ROOT/pages/5.buffers/5e.algorithms.adoc @@ -151,18 +151,19 @@ The algorithm fills target buffers sequentially, reading from source buffers as template task read_full(Stream& stream, Buffers buffers) { - consuming_buffers remaining(buffers); + auto remaining = buffer_slice(buffers); + std::size_t const total_size = buffer_size(buffers); std::size_t total = 0; - - while (buffer_size(remaining) > 0) + + while (total < total_size) { - auto [ec, n] = co_await stream.read_some(remaining); - remaining.consume(n); + auto [ec, n] = co_await stream.read_some(remaining.data()); + remaining.remove_prefix(n); total += n; if (ec) co_return total; } - + co_return total; } ---- @@ -174,18 +175,19 @@ task read_full(Stream& stream, Buffers buffers) template task write_full(Stream& stream, Buffers buffers) { - consuming_buffers remaining(buffers); + auto remaining = buffer_slice(buffers); + std::size_t const total_size = buffer_size(buffers); std::size_t total = 0; - - while (buffer_size(remaining) > 0) + + while (total < total_size) { - auto [ec, n] = co_await stream.write_some(remaining); - remaining.consume(n); + auto [ec, n] = co_await stream.write_some(remaining.data()); + remaining.remove_prefix(n); total += n; if (ec) co_return total; } - + co_return total; } ---- diff --git a/doc/modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc b/doc/modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc index 40273abcc..956817d0e 100644 --- a/doc/modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc +++ b/doc/modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc @@ -445,7 +445,7 @@ Capy has one `DynamicBuffer` concept. The v1/v2 split in Asio exists because of | Yes | -| `consuming_buffers` +| `buffer_slice` | Yes | diff --git a/doc/modules/ROOT/pages/why-capy.adoc b/doc/modules/ROOT/pages/why-capy.adoc index 225e07405..17d6f84e2 100644 --- a/doc/modules/ROOT/pages/why-capy.adoc +++ b/doc/modules/ROOT/pages/why-capy.adoc @@ -162,7 +162,7 @@ Asio got buffer sequences right. The concept-driven approach—`ConstBufferSeque Capy doesn't reinvent this. We adopt Asio's buffer sequence model because it works. -But we improve on it. Asio provides the basics; Capy extends them. Need to trim bytes from the front of a buffer sequence? Asio makes you work for it. Capy provides `slice`, `front`, `consuming_buffers`—customization points for efficient byte-level manipulation. Need a circular buffer for protocol parsing? Capy has `circular_dynamic_buffer`. Need to compose two buffers without copying? `buffer_pair`. +But we improve on it. Asio provides the basics; Capy extends them. Need to trim bytes from the front of a buffer sequence? Asio makes you work for it. Capy provides `buffer_slice` and `front`—byte-range slicing primitives for efficient byte-level manipulation. Need a circular buffer for protocol parsing? Capy has `circular_dynamic_buffer`. Need to compose two buffers without copying? `buffer_pair`. And then there's the `DynamicBuffer` mess. If you've used Asio, you've encountered the confusing split between `DynamicBuffer_v1` and `DynamicBuffer_v2`. This exists because of a fundamental problem: when an async operation takes a buffer by value and completes via callback, who owns the buffer? The original design had flaws. The "fix" created two incompatible versions. (See https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1100r0.html[P1100R0] for the full story.) @@ -174,7 +174,7 @@ One more thing: `std::ranges` cannot help here. `ranges::size` returns the numbe * `ConstBufferSequence`, `MutableBufferSequence`, `DynamicBuffer` — core concepts (Asio-compatible) * `flat_dynamic_buffer`, `circular_dynamic_buffer`, `buffer_pair` — additional concrete types -* `slice`, `front`, `buffer_array`, `consuming_buffers` — byte-level manipulation utilities +* `buffer_slice`, `front`, `buffer_array` — byte-level manipulation utilities === Comparison @@ -232,7 +232,7 @@ One more thing: `std::ranges` cannot help here. `ranges::size` returns the numbe | | -| `consuming_buffers` +| `buffer_slice` | | | diff --git a/doc/unlisted/library-buffers.adoc b/doc/unlisted/library-buffers.adoc index 863dbf845..de00e9366 100644 --- a/doc/unlisted/library-buffers.adoc +++ b/doc/unlisted/library-buffers.adoc @@ -128,8 +128,8 @@ task echo(ReadStream auto& in, WriteStream auto& out) | `` | `buffer_copy` algorithm -| `` -| Incremental buffer consumption +| `` +| Byte sub-range slicing algorithm | `` | Contiguous dynamic buffer diff --git a/include/boost/capy.hpp b/include/boost/capy.hpp index eb1e20bc5..3f868dcdb 100644 --- a/include/boost/capy.hpp +++ b/include/boost/capy.hpp @@ -39,8 +39,8 @@ #include #include #include +#include #include -#include #include #include #include @@ -64,6 +64,7 @@ #include #include #include +#include #include #include #include diff --git a/include/boost/capy/buffers/buffer_slice.hpp b/include/boost/capy/buffers/buffer_slice.hpp new file mode 100644 index 000000000..f199c8ece --- /dev/null +++ b/include/boost/capy/buffers/buffer_slice.hpp @@ -0,0 +1,89 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +#ifndef BOOST_CAPY_BUFFERS_BUFFER_SLICE_HPP +#define BOOST_CAPY_BUFFERS_BUFFER_SLICE_HPP + +#include +#include +#include + +#include +#include + +namespace boost { +namespace capy { + +/** Return a byte-range slice of a buffer sequence. + + Constructs a view over a contiguous byte range of `seq`. The + slice exposes its current bytes via `data()` (a buffer sequence) + and supports incremental consumption via `remove_prefix(n)` and + `remove_suffix(n)`. + + @par Return Value + An object of unspecified type satisfying the @ref Slice concept. + Bind with `auto` and operate through the concept's members. When + `seq` models @ref MutableBufferSequence, the returned object + additionally models @ref MutableSlice. + + @par Lifetime + The returned object holds a non-owning reference to data within + `seq`. `seq` must remain valid until the returned object is + destroyed. Iterators and buffer descriptors obtained through + `data()` follow the same invalidation rules as those of `seq`. + + @par Parameters + @li `seq` The underlying buffer sequence. + @li `offset` Number of bytes to skip from the start of `seq`. + Clamped to `buffer_size(seq)`. + @li `length` Maximum number of bytes the slice will expose, + starting at `offset`. Clamped to `buffer_size(seq) - offset`. + Defaults to the maximum value of `std::size_t`, i.e. "to end". + + @par Example + @code + template< ReadStream Stream, MutableBufferSequence MB > + task< io_result< std::size_t > > + read_all( Stream& stream, MB buffers ) + { + auto s = buffer_slice( buffers ); + std::size_t const total_size = buffer_size( buffers ); + std::size_t total = 0; + while( total < total_size ) + { + auto [ec, n] = co_await stream.read_some( s.data() ); + s.remove_prefix( n ); + total += n; + if( ec ) + co_return {ec, total}; + } + co_return {{}, total}; + } + @endcode + + @see Slice, MutableSlice +*/ +template + requires MutableBufferSequence + || ConstBufferSequence +auto +buffer_slice( + BufferSequence const& seq, + std::size_t offset = 0, + std::size_t length = + (std::numeric_limits::max)()) noexcept +{ + return detail::slice_impl(seq, offset, length); +} + +} // namespace capy +} // namespace boost + +#endif diff --git a/include/boost/capy/buffers/consuming_buffers.hpp b/include/boost/capy/buffers/consuming_buffers.hpp deleted file mode 100644 index 5d4a9417d..000000000 --- a/include/boost/capy/buffers/consuming_buffers.hpp +++ /dev/null @@ -1,242 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/capy -// - -#ifndef BOOST_CAPY_BUFFERS_CONSUMING_BUFFERS_HPP -#define BOOST_CAPY_BUFFERS_CONSUMING_BUFFERS_HPP - -#include -#include - -#include -#include -#include -#include - -namespace boost { -namespace capy { - -namespace detail { - -template -struct buffer_type_for; - -template -struct buffer_type_for -{ - using type = mutable_buffer; -}; - -template - requires (!MutableBufferSequence) -struct buffer_type_for -{ - using type = const_buffer; -}; - -} // namespace detail - -/** Wrapper for consuming a buffer sequence incrementally. - - This class wraps a buffer sequence and tracks the current - position. It provides a `consume(n)` function that advances - through the sequence as bytes are processed. - - Works with both mutable and const buffer sequences. - - @tparam BufferSequence The buffer sequence type. -*/ -template - requires MutableBufferSequence || - ConstBufferSequence -class consuming_buffers -{ - using iterator_type = decltype(capy::begin(std::declval())); - using end_iterator_type = decltype(capy::end(std::declval())); - using buffer_type = typename detail::buffer_type_for::type; - - BufferSequence const& bufs_; - iterator_type it_; - end_iterator_type end_; - std::size_t consumed_ = 0; // Bytes consumed in current buffer - -public: - /** Construct from a buffer sequence. - - @param bufs The buffer sequence to wrap. - */ - explicit consuming_buffers(BufferSequence const& bufs) noexcept - : bufs_(bufs) - , it_(capy::begin(bufs)) - , end_(capy::end(bufs)) - { - } - - /** Consume n bytes from the buffer sequence. - - Advances the current position by n bytes, moving to the - next buffer when the current one is exhausted. - - @param n The number of bytes to consume. - */ - void consume(std::size_t n) noexcept - { - while (n > 0 && it_ != end_) - { - auto const& buf = *it_; - std::size_t const buf_size = buf.size(); - std::size_t const remaining = buf_size - consumed_; - - if (n < remaining) - { - // Consume part of current buffer - consumed_ += n; - n = 0; - } - else - { - // Consume rest of current buffer and move to next - n -= remaining; - consumed_ = 0; - ++it_; - } - } - } - - /** Iterator for the consuming buffer sequence. - - Returns buffers starting from the current position, - with the first buffer adjusted for consumed bytes. - */ - class const_iterator - { - iterator_type it_; - end_iterator_type end_; - std::size_t consumed_; - - public: - using iterator_category = std::bidirectional_iterator_tag; - using value_type = buffer_type; - using difference_type = std::ptrdiff_t; - using pointer = value_type*; - using reference = value_type; - - // Default constructor required for forward_iterator - const_iterator() noexcept = default; - - /// Construct from position and consumed byte count. - const_iterator( - iterator_type it, - end_iterator_type end, - std::size_t consumed) noexcept - : it_(it) - , end_(end) - , consumed_(consumed) - { - } - - /// Test for equality. - bool operator==(const_iterator const& other) const noexcept - { - return it_ == other.it_ && consumed_ == other.consumed_; - } - - /// Test for inequality. - bool operator!=(const_iterator const& other) const noexcept - { - return !(*this == other); - } - - /// Return the current buffer, adjusted for consumed bytes. - value_type operator*() const noexcept - { - auto const& buf = *it_; - if constexpr (std::is_same_v) - { - return buffer_type( - static_cast(buf.data()) + consumed_, - buf.size() - consumed_); - } - else - { - return buffer_type( - static_cast(buf.data()) + consumed_, - buf.size() - consumed_); - } - } - - /// Advance to the next element. - const_iterator& operator++() noexcept - { - consumed_ = 0; - ++it_; - return *this; - } - - /// Advance to the next element (postfix). - const_iterator operator++(int) noexcept - { - const_iterator tmp = *this; - ++*this; - return tmp; - } - - /// Move to the previous element. - const_iterator& operator--() noexcept - { - if (consumed_ == 0) - { - --it_; - // Set consumed_ to the size of the previous buffer - // This is a simplified implementation for bidirectional requirement - if (it_ != end_) - { - auto const& buf = *it_; - consumed_ = buf.size(); - } - } - else - { - consumed_ = 0; - } - return *this; - } - - /// Move to the previous element (postfix). - const_iterator operator--(int) noexcept - { - const_iterator tmp = *this; - --*this; - return tmp; - } - }; - - /** Return iterator to beginning of remaining buffers. - - @return Iterator pointing to the first remaining buffer, - adjusted for consumed bytes in the current buffer. - */ - const_iterator begin() const noexcept - { - return const_iterator(it_, end_, consumed_); - } - - /** Return iterator to end of buffer sequence. - - @return End iterator. - */ - const_iterator end() const noexcept - { - return const_iterator(end_, end_, 0); - } -}; - -} // namespace capy -} // namespace boost - -#endif diff --git a/include/boost/capy/concept/slice.hpp b/include/boost/capy/concept/slice.hpp new file mode 100644 index 000000000..f552710ec --- /dev/null +++ b/include/boost/capy/concept/slice.hpp @@ -0,0 +1,127 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +#ifndef BOOST_CAPY_CONCEPT_SLICE_HPP +#define BOOST_CAPY_CONCEPT_SLICE_HPP + +#include +#include + +#include + +namespace boost { +namespace capy { + +/** Concept for types that view a byte sub-range of a buffer sequence. + + A type satisfies `Slice` if it provides a view over a contiguous + byte range within an underlying buffer sequence, with an operation + to advance the start and exposure of the current bytes as a buffer + sequence. + + @par Syntactic Requirements + @li `cs.data()` returns a @ref ConstBufferSequence + @li `s.remove_prefix(n)` advances the start of the slice by `n` bytes + + @par Semantic Requirements + @li `s.data()` returns a buffer sequence view of the slice's current + live bytes. The view is valid until the next mutating operation + on `s` or `s`'s destruction. + @li `s.remove_prefix(n)` makes the first `min(n, total_live_bytes)` + bytes no longer part of the slice. + + @par Lifetime + The underlying buffer sequence referenced by the slice must outlive + any `Slice`-modeling object that views it. Iterators and buffer + descriptors obtained through `data()` follow the same invalidation + rules as those of the underlying sequence. + + @par Concrete Types + Objects modeling `Slice` are produced by the @ref buffer_slice free + function. The concrete type returned by `buffer_slice` is unspecified; + user code should bind it with `auto` and rely on this concept. When + the underlying buffer sequence models @ref MutableBufferSequence, the + returned object additionally models @ref MutableSlice. + + @par Example + @code + template< WriteStream Stream, Slice S > + task<> write_all( Stream& stream, S s, std::size_t total ) + { + std::size_t sent = 0; + while( sent < total ) + { + auto [ec, n] = co_await stream.write_some( s.data() ); + s.remove_prefix( n ); + sent += n; + if( ec ) + co_return; + } + } + @endcode + + @see buffer_slice, MutableSlice, ConstBufferSequence +*/ +template +concept Slice = + requires(T& s, T const& cs, std::size_t n) + { + { cs.data() } -> ConstBufferSequence; + s.remove_prefix(n); + }; + +/** Concept for slices whose `data()` exposes writable buffers. + + A type satisfies `MutableSlice` if it satisfies @ref Slice and + its `data()` member additionally returns a + @ref MutableBufferSequence. This is the slice analog of the + @ref MutableBufferSequence refinement of @ref ConstBufferSequence. + + Use `MutableSlice` to constrain generic code that needs to pass + the slice's current bytes to a @ref ReadStream's `read_some` or + any other operation requiring write access through the buffer + sequence. + + @par Producing a MutableSlice + @ref buffer_slice returns an object modeling `MutableSlice` when + the input buffer sequence models @ref MutableBufferSequence. When + the input is only @ref ConstBufferSequence, the returned object + models @ref Slice but not `MutableSlice`. + + @par Example + @code + template< ReadStream Stream, MutableSlice S > + task<> read_all( Stream& stream, S s, std::size_t total ) + { + std::size_t received = 0; + while( received < total ) + { + auto [ec, n] = co_await stream.read_some( s.data() ); + s.remove_prefix( n ); + received += n; + if( ec ) + co_return; + } + } + @endcode + + @see Slice, buffer_slice, MutableBufferSequence +*/ +template +concept MutableSlice = + Slice && + requires(T const& cs) + { + { cs.data() } -> MutableBufferSequence; + }; + +} // namespace capy +} // namespace boost + +#endif diff --git a/include/boost/capy/detail/slice_impl.hpp b/include/boost/capy/detail/slice_impl.hpp new file mode 100644 index 000000000..615ef780b --- /dev/null +++ b/include/boost/capy/detail/slice_impl.hpp @@ -0,0 +1,305 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +/* + Implementation type for the public buffer_slice() free function. + Users see this only via auto + the Slice concept; the type is + documented as unspecified. Maintained alongside Slice in + include/boost/capy/concept/slice.hpp. +*/ + +#ifndef BOOST_CAPY_DETAIL_SLICE_IMPL_HPP +#define BOOST_CAPY_DETAIL_SLICE_IMPL_HPP + +#include +#include + +#include +#include +#include + +namespace boost { +namespace capy { +namespace detail { + +template +struct slice_buffer_type_for; + +template +struct slice_buffer_type_for +{ + using type = mutable_buffer; +}; + +template + requires (!MutableBufferSequence) +struct slice_buffer_type_for +{ + using type = const_buffer; +}; + +template + requires MutableBufferSequence + || ConstBufferSequence +class slice_impl +{ +public: + using iterator_type = + decltype(capy::begin(std::declval())); + using end_iterator_type = + decltype(capy::end(std::declval())); + using buffer_type = + typename slice_buffer_type_for::type; + +private: + iterator_type first_{}; + end_iterator_type last_{}; + std::size_t front_skip_ = 0; + std::size_t back_skip_ = 0; + + static buffer_type adjust_buffer( + buffer_type const& buf, + std::size_t front, + std::size_t back) noexcept + { + if constexpr (std::is_same_v) + { + return mutable_buffer( + static_cast(buf.data()) + front, + buf.size() - front - back); + } + else + { + return const_buffer( + static_cast(buf.data()) + front, + buf.size() - front - back); + } + } + +public: + /// View returned by `slice_impl::data()`. + class data_view + { + iterator_type first_{}; + end_iterator_type last_{}; + std::size_t front_skip_ = 0; + std::size_t back_skip_ = 0; + + public: + class const_iterator + { + iterator_type cur_{}; + iterator_type anchor_first_{}; + end_iterator_type anchor_last_{}; + std::size_t front_skip_ = 0; + std::size_t back_skip_ = 0; + + public: + using iterator_category = std::bidirectional_iterator_tag; + using value_type = buffer_type; + using difference_type = std::ptrdiff_t; + using pointer = value_type*; + using reference = value_type; + + const_iterator() noexcept = default; + + const_iterator( + iterator_type cur, + iterator_type anchor_first, + end_iterator_type anchor_last, + std::size_t front_skip, + std::size_t back_skip) noexcept + : cur_(cur) + , anchor_first_(anchor_first) + , anchor_last_(anchor_last) + , front_skip_(front_skip) + , back_skip_(back_skip) + { + } + + bool operator==(const_iterator const& other) const noexcept + { + return cur_ == other.cur_; + } + + bool operator!=(const_iterator const& other) const noexcept + { + return !(*this == other); + } + + value_type operator*() const noexcept + { + buffer_type buf = *cur_; + auto front = (cur_ == anchor_first_) ? front_skip_ : 0; + auto next = cur_; + ++next; + auto back = (next == anchor_last_) ? back_skip_ : 0; + return adjust_buffer(buf, front, back); + } + + const_iterator& operator++() noexcept + { + ++cur_; + return *this; + } + + const_iterator operator++(int) noexcept + { + const_iterator tmp = *this; + ++*this; + return tmp; + } + + const_iterator& operator--() noexcept + { + --cur_; + return *this; + } + + const_iterator operator--(int) noexcept + { + const_iterator tmp = *this; + --*this; + return tmp; + } + }; + + data_view() noexcept = default; + + data_view( + iterator_type first, + end_iterator_type last, + std::size_t front_skip, + std::size_t back_skip) noexcept + : first_(first) + , last_(last) + , front_skip_(front_skip) + , back_skip_(back_skip) + { + } + + const_iterator begin() const noexcept + { + return const_iterator( + first_, first_, last_, front_skip_, back_skip_); + } + + const_iterator end() const noexcept + { + return const_iterator( + last_, first_, last_, front_skip_, back_skip_); + } + }; + + slice_impl() noexcept = default; + + explicit slice_impl(BufferSequence const& bs) noexcept + : first_(capy::begin(bs)) + , last_(capy::end(bs)) + { + } + + slice_impl( + BufferSequence const& bs, + std::size_t offset, + std::size_t length) noexcept + { + auto it_begin = capy::begin(bs); + auto it_end = capy::end(bs); + + std::size_t total = 0; + for (auto it = it_begin; it != it_end; ++it) + total += (*it).size(); + + if (offset > total) + offset = total; + std::size_t const remaining = total - offset; + if (length > remaining) + length = remaining; + + first_ = it_begin; + last_ = it_end; + + std::size_t skip = offset; + while (first_ != last_) + { + std::size_t const buf_size = (*first_).size(); + if (skip < buf_size) + { + front_skip_ = skip; + break; + } + skip -= buf_size; + ++first_; + } + + std::size_t left = length; + auto cursor = first_; + std::size_t cursor_front = front_skip_; + while (cursor != last_ && left > 0) + { + std::size_t const buf_size = (*cursor).size(); + std::size_t const avail = buf_size - cursor_front; + if (left <= avail) + { + back_skip_ = avail - left; + ++cursor; + last_ = cursor; + return; + } + left -= avail; + ++cursor; + cursor_front = 0; + } + + last_ = cursor; + } + + data_view data() const noexcept + { + return data_view(first_, last_, front_skip_, back_skip_); + } + + void remove_prefix(std::size_t n) noexcept + { + while (n > 0 && first_ != last_) + { + std::size_t const buf_total = (*first_).size(); + std::size_t live = buf_total - front_skip_; + auto next = first_; + ++next; + bool const is_last = (next == last_); + if (is_last) + live -= back_skip_; + + if (n < live) + { + front_skip_ += n; + return; + } + + n -= live; + if (is_last) + { + first_ = last_; + front_skip_ = 0; + back_skip_ = 0; + return; + } + ++first_; + front_skip_ = 0; + } + } +}; + +} // namespace detail +} // namespace capy +} // namespace boost + +#endif diff --git a/include/boost/capy/io/write_now.hpp b/include/boost/capy/io/write_now.hpp index b1e430456..9e5a8cc0b 100644 --- a/include/boost/capy/io/write_now.hpp +++ b/include/boost/capy/io/write_now.hpp @@ -13,7 +13,7 @@ #include #include #include -#include +#include #include #include #include @@ -336,12 +336,12 @@ class write_now { std::size_t const total_size = buffer_size(buffers); std::size_t total_written = 0; - consuming_buffers cb(buffers); + auto cb = buffer_slice(buffers); while(total_written < total_size) { auto r = - co_await stream_.write_some(cb); - cb.consume(std::get<0>(r.values)); + co_await stream_.write_some(cb.data()); + cb.remove_prefix(std::get<0>(r.values)); total_written += std::get<0>(r.values); if(r.ec) co_return io_result{ @@ -359,19 +359,19 @@ class write_now std::size_t total_written = 0; // GCC ICE in expand_expr_real_1 (expr.cc:11376) - // when consuming_buffers spans a co_yield, so + // when the buffer slice spans a co_yield, so // the GCC path uses a separate simple coroutine. - consuming_buffers cb(buffers); + auto cb = buffer_slice(buffers); while(total_written < total_size) { - auto inner = stream_.write_some(cb); + auto inner = stream_.write_some(cb.data()); if(!inner.await_ready()) break; auto r = inner.await_resume(); if(r.ec) co_return io_result{ r.ec, total_written}; - cb.consume(std::get<0>(r.values)); + cb.remove_prefix(std::get<0>(r.values)); total_written += std::get<0>(r.values); } @@ -384,8 +384,8 @@ class write_now while(total_written < total_size) { auto r = - co_await stream_.write_some(cb); - cb.consume(std::get<0>(r.values)); + co_await stream_.write_some(cb.data()); + cb.remove_prefix(std::get<0>(r.values)); total_written += std::get<0>(r.values); if(r.ec) co_return io_result{ diff --git a/include/boost/capy/read.hpp b/include/boost/capy/read.hpp index ea163084b..3699d0d48 100644 --- a/include/boost/capy/read.hpp +++ b/include/boost/capy/read.hpp @@ -14,7 +14,7 @@ #include #include #include -#include +#include #include #include #include @@ -73,14 +73,14 @@ read( MutableBufferSequence auto const& buffers) -> io_task { - consuming_buffers consuming(buffers); + auto consuming = buffer_slice(buffers); std::size_t const total_size = buffer_size(buffers); std::size_t total_read = 0; while(total_read < total_size) { - auto [ec, n] = co_await stream.read_some(consuming); - consuming.consume(n); + auto [ec, n] = co_await stream.read_some(consuming.data()); + consuming.remove_prefix(n); total_read += n; if(ec) co_return {ec, total_read}; diff --git a/include/boost/capy/write.hpp b/include/boost/capy/write.hpp index bed7fba99..42534b6f7 100644 --- a/include/boost/capy/write.hpp +++ b/include/boost/capy/write.hpp @@ -13,7 +13,7 @@ #include #include #include -#include +#include #include #include @@ -67,14 +67,14 @@ write( ConstBufferSequence auto const& buffers) -> io_task { - consuming_buffers consuming(buffers); + auto consuming = buffer_slice(buffers); std::size_t const total_size = buffer_size(buffers); std::size_t total_written = 0; while(total_written < total_size) { - auto [ec, n] = co_await stream.write_some(consuming); - consuming.consume(n); + auto [ec, n] = co_await stream.write_some(consuming.data()); + consuming.remove_prefix(n); total_written += n; if(ec) co_return {ec, total_written}; diff --git a/test/unit/buffers/buffer_slice.cpp b/test/unit/buffers/buffer_slice.cpp new file mode 100644 index 000000000..371e1246b --- /dev/null +++ b/test/unit/buffers/buffer_slice.cpp @@ -0,0 +1,423 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +// Test that headers are self-contained. +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include "test_suite.hpp" + +namespace boost { +namespace capy { + +namespace { + +// Flatten the bytes exposed by a Slice's data() into a std::string for +// byte-exact comparison. +template +std::string flatten(Slice const& s) +{ + std::string out; + auto view = s.data(); + for (auto it = view.begin(); it != view.end(); ++it) + { + auto buf = *it; + out.append( + static_cast(buf.data()), + buf.size()); + } + return out; +} + +} // anonymous namespace + +struct buffer_slice_test +{ + void + testConceptModeled() + { + char a[10]; + std::array mbufs = { + mutable_buffer(a, sizeof(a)) + }; + std::array cbufs = { + const_buffer(a, sizeof(a)) + }; + using m_slice = detail::slice_impl; + using c_slice = detail::slice_impl; + + // Both satisfy Slice + static_assert(Slice, + "mutable-input slice_impl must satisfy Slice"); + static_assert(Slice, + "const-input slice_impl must satisfy Slice"); + + // Only the mutable-input one satisfies MutableSlice + static_assert(MutableSlice, + "mutable-input slice_impl must satisfy MutableSlice"); + static_assert(!MutableSlice, + "const-input slice_impl must NOT satisfy MutableSlice"); + } + + void + testNotABufferSequence() + { + char a[10]; + std::array bufs = { + mutable_buffer(a, sizeof(a)) + }; + using slice_t = detail::slice_impl; + static_assert( + !ConstBufferSequence, + "slice_impl must not model ConstBufferSequence"); + static_assert( + !MutableBufferSequence, + "slice_impl must not model MutableBufferSequence"); + } + + void + testDataIsBufferSequence() + { + char a[10]; + std::array bufs = { + mutable_buffer(a, sizeof(a)) + }; + detail::slice_impl s(bufs); + using data_t = decltype(s.data()); + static_assert( + MutableBufferSequence, + "data() return must satisfy MutableBufferSequence " + "when input is mutable"); + static_assert( + ConstBufferSequence, + "data() return must satisfy ConstBufferSequence"); + static_assert( + std::ranges::bidirectional_range, + "data() return must be a bidirectional_range"); + } + + void + testWholeSequenceCtor() + { + char a[10]; + char b[20]; + std::memset(a, 'A', sizeof(a)); + std::memset(b, 'B', sizeof(b)); + std::array bufs = { + mutable_buffer(a, sizeof(a)), + mutable_buffer(b, sizeof(b)) + }; + detail::slice_impl s(bufs); + BOOST_TEST_EQ(buffer_size(s.data()), sizeof(a) + sizeof(b)); + + std::string const expected = + std::string(sizeof(a), 'A') + std::string(sizeof(b), 'B'); + BOOST_TEST_EQ(flatten(s), expected); + } + + void + testOffsetLengthCtor() + { + char a[10]; + char b[20]; + std::memset(a, 'A', sizeof(a)); + std::memset(b, 'B', sizeof(b)); + std::array bufs = { + mutable_buffer(a, sizeof(a)), + mutable_buffer(b, sizeof(b)) + }; + using slice_t = detail::slice_impl; + + // offset=0, length=total -> whole sequence + { + slice_t s(bufs, 0, 30); + BOOST_TEST_EQ(buffer_size(s.data()), 30u); + BOOST_TEST_EQ(flatten(s), + std::string(10, 'A') + std::string(20, 'B')); + } + + // offset inside first buffer (front trim, no back trim) + { + slice_t s(bufs, 3, 27); + BOOST_TEST_EQ(buffer_size(s.data()), 27u); + BOOST_TEST_EQ(flatten(s), + std::string(7, 'A') + std::string(20, 'B')); + } + + // offset past first buffer, length terminating inside last (front + back) + { + slice_t s(bufs, 12, 5); + BOOST_TEST_EQ(buffer_size(s.data()), 5u); + BOOST_TEST_EQ(flatten(s), std::string(5, 'B')); + } + + // both offset and length inside first buffer + { + slice_t s(bufs, 2, 4); + BOOST_TEST_EQ(buffer_size(s.data()), 4u); + BOOST_TEST_EQ(flatten(s), std::string(4, 'A')); + } + + // offset=0, length=0 -> empty + { + slice_t s(bufs, 0, 0); + BOOST_TEST_EQ(buffer_size(s.data()), 0u); + BOOST_TEST_EQ(flatten(s), std::string()); + } + + // offset >= total -> empty (no UB) + { + slice_t s(bufs, 50, 10); + BOOST_TEST_EQ(buffer_size(s.data()), 0u); + } + + // length > total - offset -> clamped to remainder + { + slice_t s(bufs, 5, 999); + BOOST_TEST_EQ(buffer_size(s.data()), 25u); + BOOST_TEST_EQ(flatten(s), + std::string(5, 'A') + std::string(20, 'B')); + } + } + + void + testRemovePrefix() + { + char a[10]; + char b[20]; + std::memset(a, 'A', sizeof(a)); + std::memset(b, 'B', sizeof(b)); + std::array bufs = { + mutable_buffer(a, sizeof(a)), + mutable_buffer(b, sizeof(b)) + }; + using slice_t = detail::slice_impl; + + // remove within first buffer + { + slice_t s(bufs); + s.remove_prefix(3); + BOOST_TEST_EQ(buffer_size(s.data()), 27u); + BOOST_TEST_EQ(flatten(s), + std::string(7, 'A') + std::string(20, 'B')); + } + + // remove exactly to end of first buffer + { + slice_t s(bufs); + s.remove_prefix(10); + BOOST_TEST_EQ(buffer_size(s.data()), 20u); + BOOST_TEST_EQ(flatten(s), std::string(20, 'B')); + } + + // remove crossing into second buffer + { + slice_t s(bufs); + s.remove_prefix(15); + BOOST_TEST_EQ(buffer_size(s.data()), 15u); + BOOST_TEST_EQ(flatten(s), std::string(15, 'B')); + } + + // remove all + { + slice_t s(bufs); + s.remove_prefix(30); + BOOST_TEST_EQ(buffer_size(s.data()), 0u); + } + + // remove more than total -> empty, no UB + { + slice_t s(bufs); + s.remove_prefix(1000); + BOOST_TEST_EQ(buffer_size(s.data()), 0u); + } + } + + void + testRemovePrefixOnLengthCapped() + { + // Verify remove_prefix walks correctly through a slice that has + // back_skip_ set by an offset/length constructor. + char a[5]; + char b[5]; + char c[5]; + std::memset(a, 'a', sizeof(a)); + std::memset(b, 'b', sizeof(b)); + std::memset(c, 'c', sizeof(c)); + std::array bufs = { + mutable_buffer(a, sizeof(a)), + mutable_buffer(b, sizeof(b)), + mutable_buffer(c, sizeof(c)) + }; + using slice_t = detail::slice_impl; + + // bytes 2..12 -> [3 'a' + 5 'b' + 2 'c'] + slice_t s(bufs, 2, 10); + BOOST_TEST_EQ(buffer_size(s.data()), 10u); + BOOST_TEST_EQ(flatten(s), + std::string(3, 'a') + std::string(5, 'b') + std::string(2, 'c')); + + // remove 4 -> [4 'b' + 2 'c'] (consumed 3 'a' + 1 'b') + s.remove_prefix(4); + BOOST_TEST_EQ(buffer_size(s.data()), 6u); + BOOST_TEST_EQ(flatten(s), + std::string(4, 'b') + std::string(2, 'c')); + + // remove 5 -> [1 'c'] (consumed 4 'b' + 1 'c') + s.remove_prefix(5); + BOOST_TEST_EQ(buffer_size(s.data()), 1u); + BOOST_TEST_EQ(flatten(s), std::string(1, 'c')); + + // remove 1 -> empty + s.remove_prefix(1); + BOOST_TEST_EQ(buffer_size(s.data()), 0u); + } + + void + testEmpty() + { + // default-constructed slice + detail::slice_impl> s{}; + BOOST_TEST_EQ(buffer_size(s.data()), 0u); + s.remove_prefix(5); + BOOST_TEST_EQ(buffer_size(s.data()), 0u); + } + + void + testMutableVsConst() + { + char a[10]; + std::array mbufs = { + mutable_buffer(a, sizeof(a)) + }; + std::array cbufs = { + const_buffer(a, sizeof(a)) + }; + using m_slice = detail::slice_impl; + using c_slice = detail::slice_impl; + + static_assert( + std::is_same_v, + "mutable input -> mutable buffer_type"); + static_assert( + std::is_same_v, + "const input -> const buffer_type"); + + m_slice ms(mbufs); + c_slice cs(cbufs); + BOOST_TEST_EQ(buffer_size(ms.data()), 10u); + BOOST_TEST_EQ(buffer_size(cs.data()), 10u); + } + + void + testSingleBuffer() + { + char a[10]; + std::memset(a, 'X', sizeof(a)); + mutable_buffer mb(a, sizeof(a)); + + detail::slice_impl s(mb); + BOOST_TEST_EQ(buffer_size(s.data()), 10u); + BOOST_TEST_EQ(flatten(s), std::string(10, 'X')); + + s.remove_prefix(3); + BOOST_TEST_EQ(buffer_size(s.data()), 7u); + BOOST_TEST_EQ(flatten(s), std::string(7, 'X')); + } + + void + testPublicFunction() + { + char a[10]; + char b[20]; + std::memset(a, 'A', sizeof(a)); + std::memset(b, 'B', sizeof(b)); + std::array bufs = { + mutable_buffer(a, sizeof(a)), + mutable_buffer(b, sizeof(b)) + }; + + // default args: whole sequence + { + auto s = buffer_slice(bufs); + static_assert(Slice, + "buffer_slice's return must satisfy Slice"); + static_assert(MutableSlice, + "buffer_slice over mutable input must satisfy MutableSlice"); + BOOST_TEST_EQ(buffer_size(s.data()), 30u); + BOOST_TEST_EQ(flatten(s), + std::string(10, 'A') + std::string(20, 'B')); + } + + // const input -> Slice but not MutableSlice + { + std::array cbufs = { + const_buffer(a, sizeof(a)) + }; + auto s = buffer_slice(cbufs); + static_assert(Slice, + "buffer_slice over const input must satisfy Slice"); + static_assert(!MutableSlice, + "buffer_slice over const input must NOT satisfy MutableSlice"); + BOOST_TEST_EQ(buffer_size(s.data()), 10u); + } + + // offset + length + { + auto s = buffer_slice(bufs, 5, 10); + BOOST_TEST_EQ(buffer_size(s.data()), 10u); + BOOST_TEST_EQ(flatten(s), + std::string(5, 'A') + std::string(5, 'B')); + } + + // offset only (length defaults to "to end") + { + auto s = buffer_slice(bufs, 12); + BOOST_TEST_EQ(buffer_size(s.data()), 18u); + BOOST_TEST_EQ(flatten(s), std::string(18, 'B')); + } + + // single buffer + { + mutable_buffer mb(a, sizeof(a)); + auto s = buffer_slice(mb, 2, 5); + BOOST_TEST_EQ(buffer_size(s.data()), 5u); + BOOST_TEST_EQ(flatten(s), std::string(5, 'A')); + } + } + + void + run() + { + testConceptModeled(); + testNotABufferSequence(); + testDataIsBufferSequence(); + testWholeSequenceCtor(); + testOffsetLengthCtor(); + testRemovePrefix(); + testRemovePrefixOnLengthCapped(); + testEmpty(); + testMutableVsConst(); + testSingleBuffer(); + testPublicFunction(); + } +}; + +TEST_SUITE(buffer_slice_test, "boost.capy.buffer_slice"); + +} // namespace capy +} // namespace boost diff --git a/test/unit/buffers/consuming_buffers.cpp b/test/unit/buffers/consuming_buffers.cpp deleted file mode 100644 index b04b5b6ed..000000000 --- a/test/unit/buffers/consuming_buffers.cpp +++ /dev/null @@ -1,136 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/capy -// - -// Test that header file is self-contained. -#include - -#include - -#include -#include -#include -#include - -#include "test_suite.hpp" - -namespace boost { -namespace capy { - -//------------------------------------------------ -// consuming_buffers tests -// Focus: verify consuming_buffers models buffer sequence concept -//------------------------------------------------ - -struct consuming_buffers_test -{ - void - testBufferSequenceConcept() - { - char buf1[100]; - char buf2[200]; - std::array bufs = { - mutable_buffer(buf1, sizeof(buf1)), - mutable_buffer(buf2, sizeof(buf2)) - }; - - consuming_buffers cb(bufs); - - // Verify consuming_buffers models mutable_buffer_sequence - static_assert( - MutableBufferSequence>, - "consuming_buffers must model mutable_buffer_sequence"); - - // Verify it can be used with buffer_size - std::size_t const size = buffer_size(cb); - BOOST_TEST_EQ(size, sizeof(buf1) + sizeof(buf2)); - } - - void - testSingleBuffer() - { - char buf[100]; - mutable_buffer mbuf(buf, sizeof(buf)); - - consuming_buffers cb(mbuf); - - // Verify consuming_buffers models mutable_buffer_sequence for single buffer - static_assert( - MutableBufferSequence>, - "consuming_buffers must model mutable_buffer_sequence for single buffer"); - - std::size_t const size = buffer_size(cb); - BOOST_TEST_EQ(size, sizeof(buf)); - } - - void - testRangeConcepts() - { - char buf1[100]; - char buf2[200]; - std::array bufs = { - mutable_buffer(buf1, sizeof(buf1)), - mutable_buffer(buf2, sizeof(buf2)) - }; - - using cb_type = consuming_buffers; - - // Most general to most specific - Range Concepts - static_assert(std::ranges::range, - "consuming_buffers must satisfy std::ranges::range"); - static_assert(std::ranges::input_range, - "consuming_buffers must satisfy std::ranges::input_range"); - static_assert(std::ranges::forward_range, - "consuming_buffers must satisfy std::ranges::forward_range"); - static_assert(std::ranges::bidirectional_range, - "consuming_buffers must satisfy std::ranges::bidirectional_range"); - - // Most general to most specific - Iterator Concepts - using iter_t = std::ranges::iterator_t; - static_assert(std::input_iterator, - "consuming_buffers iterator must satisfy std::input_iterator"); - static_assert(std::forward_iterator, - "consuming_buffers iterator must satisfy std::forward_iterator"); - static_assert(std::bidirectional_iterator, - "consuming_buffers iterator must satisfy std::bidirectional_iterator"); - - // Iterator traits check - using traits = std::iterator_traits; - static_assert(std::same_as, - "Iterator category must be bidirectional_iterator_tag"); - - // Range value type check - static_assert(std::is_convertible_v, mutable_buffer>, - "Range value type must be convertible to mutable_buffer"); - - // Verify std::ranges::begin and std::ranges::end work - { - cb_type cb(bufs); - auto it1 = std::ranges::begin(cb); - auto it2 = std::ranges::end(cb); - BOOST_TEST(it1 != it2); - } - - // Final check - Buffer Sequence Concept - static_assert(MutableBufferSequence, - "consuming_buffers must model mutable_buffer_sequence"); - } - - void - run() - { - testBufferSequenceConcept(); - testSingleBuffer(); - testRangeConcepts(); - } -}; - -TEST_SUITE(consuming_buffers_test, "boost.capy.consuming_buffers"); - -} // namespace capy -} // namespace boost