From 27d7b3cd1ff3ddc489c15a3c6436b3a860d353ac Mon Sep 17 00:00:00 2001 From: Nikolay Malkovsky Date: Mon, 31 Aug 2026 15:07:44 +0300 Subject: [PATCH 1/3] Add segmented sliding-window storage --- include/pixie/split_span.h | 74 +++++++ include/pixie/storage.h | 112 +++++++++- include/pixie/storage/aligned.h | 18 ++ include/pixie/storage/implementations.h | 2 + include/pixie/storage/read_only_view.h | 12 ++ include/pixie/storage/sliding_window.h | 157 ++++++++++++++ src/benchmarks/serialization_benchmarks.cpp | 93 +++++++++ src/tests/storage_tests.cpp | 216 ++++++++++++++++++++ 8 files changed, 677 insertions(+), 7 deletions(-) create mode 100644 include/pixie/split_span.h create mode 100644 include/pixie/storage/sliding_window.h diff --git a/include/pixie/split_span.h b/include/pixie/split_span.h new file mode 100644 index 0000000..8a939f2 --- /dev/null +++ b/include/pixie/split_span.h @@ -0,0 +1,74 @@ +#pragma once + +/** + * @file split_span.h + * @brief Allocation-free logical ranges split across at most two spans. + */ + +#include +#include +#include + +namespace pixie { + +/** + * @brief A logical contiguous sequence stored in at most two physical spans. + * + * @details Empty input spans are canonicalized away. Iteration visits only + * non-empty physical spans in logical order. The descriptor owns no elements; + * its spans follow the lifetime and invalidation rules of their backing + * storage. + * + * @tparam T Viewed element type, optionally const-qualified. + */ +template +class SplitSpan { + public: + using span_type = std::span; + using const_iterator = typename std::array::const_iterator; + + /** @brief Construct an empty logical range. */ + constexpr SplitSpan() = default; + + /** @brief Construct a range stored in one physical span. */ + constexpr explicit SplitSpan(span_type segment) + : segments_{segment, {}}, segment_count_(segment.empty() ? 0 : 1) {} + + /** @brief Construct a range stored in up to two physical spans. */ + constexpr SplitSpan(span_type first, span_type second) { + if (first.empty()) { + first = second; + second = {}; + } + segments_ = {first, second}; + segment_count_ = static_cast(!first.empty()) + + static_cast(!second.empty()); + } + + /** @brief Return the total number of logical elements. */ + constexpr std::size_t size() const noexcept { + return segments_[0].size() + segments_[1].size(); + } + + /** @brief Return whether the logical range is empty. */ + constexpr bool empty() const noexcept { return segment_count_ == 0; } + + /** @brief Return the number of non-empty physical segments. */ + constexpr std::size_t segment_count() const noexcept { + return segment_count_; + } + + /** @brief Iterate over the non-empty physical segments in logical order. */ + constexpr const_iterator begin() const noexcept { return segments_.begin(); } + + /** @brief Return the end iterator for the non-empty physical segments. */ + constexpr const_iterator end() const noexcept { + return segments_.begin() + static_cast(segment_count_); + } + + private: + std::array segments_{}; + std::size_t segment_count_ = 0; +}; + +} // namespace pixie diff --git a/include/pixie/storage.h b/include/pixie/storage.h index 4cd03c4..50ea0f2 100644 --- a/include/pixie/storage.h +++ b/include/pixie/storage.h @@ -9,6 +9,7 @@ */ #include +#include #include #include @@ -26,6 +27,9 @@ namespace pixie { template class StorageBase : public SerializationBase { public: + /** @brief Monotonic logical byte position used to address storage ranges. */ + using position_type = std::uint64_t; + /** * @brief Return the logical exposed storage size in bytes. * @details An owning implementation may reserve or pad more memory; use @@ -39,15 +43,88 @@ class StorageBase : public SerializationBase { /** @brief Check whether the storage is empty. */ bool empty() const { return size_bytes() == 0; } - /** @brief Return a read-only view of all logical exposed bytes. */ - std::span as_bytes() const { return impl().as_bytes_impl(); } + /** @brief Return the first logical byte position currently exposed. */ + position_type begin_position() const { return impl().begin_position_impl(); } + + /** @brief Return the position one past the last logical byte exposed. */ + position_type end_position() const { return impl().end_position_impl(); } + + /** @brief Return whether a complete logical range is currently exposed. */ + bool contains(position_type position, std::size_t count_bytes) const { + const position_type begin = begin_position(); + const position_type end = end_position(); + return position >= begin && position <= end && + count_bytes <= end - position; + } + + /** @brief Return a contiguous read-only view of all logical exposed bytes. */ + std::span as_bytes() const + requires requires(const Impl& value) { value.as_bytes_impl(); } + { + return impl().as_bytes_impl(); + } + + /** + * @brief Return all logical bytes as one or two writable physical spans. + * @details Available only for mutable storage implementations. Mutating or + * resizing the storage may invalidate the returned descriptor. + */ + SplitSpan segments() + requires requires(Impl& value) { + { + value.segments_impl(position_type{}, std::size_t{}) + } -> std::same_as>; + } + { + return segments(begin_position(), size_bytes()); + } + + /** + * @brief Return all logical bytes as one or two physical spans. + * @details Contiguous storage returns one segment. A ring-backed storage can + * return its tail followed by its head without allocation or copying. + */ + SplitSpan segments() const { + return segments(begin_position(), size_bytes()); + } + + /** + * @brief Return a checked writable logical byte range as one or two spans. + * @param position First logical byte position in the range. + * @param count_bytes Number of logical bytes in the range. + * @throws std::out_of_range if the range is outside this storage. + */ + SplitSpan segments(position_type position, std::size_t count_bytes) + requires requires(Impl& value) { + { + value.segments_impl(position_type{}, std::size_t{}) + } -> std::same_as>; + } + { + validate_range(position, count_bytes); + return impl().segments_impl(position, count_bytes); + } + + /** + * @brief Return a checked logical byte range as one or two physical spans. + * @param position First logical byte position in the range. + * @param count_bytes Number of logical bytes in the range. + * @throws std::out_of_range if the range is outside this storage. + */ + SplitSpan segments(position_type position, + std::size_t count_bytes) const { + validate_range(position, count_bytes); + return impl().segments_impl(position, count_bytes); + } /** * @brief Return a read-only view as 16-bit words. * @throws std::invalid_argument if the data is misaligned or its size is not * divisible by the word size. */ - std::span as_words16() const { + std::span as_words16() const + requires requires(const Impl& value) { value.as_bytes_impl(); } + { return as_words(); } @@ -56,12 +133,20 @@ class StorageBase : public SerializationBase { * @throws std::invalid_argument if the data is misaligned or its size is not * divisible by the word size. */ - std::span as_words64() const { + std::span as_words64() const + requires requires(const Impl& value) { value.as_bytes_impl(); } + { return as_words(); } /** @brief Return a non-owning read-only view of all exposed bytes. */ - auto view() const { return impl().view_impl(0, size_bytes()); } + auto view() const + requires requires(const Impl& value) { + value.view_impl(std::size_t{}, std::size_t{}); + } + { + return impl().view_impl(0, size_bytes()); + } /** * @brief Return a non-owning read-only byte subrange. @@ -69,7 +154,11 @@ class StorageBase : public SerializationBase { * @param count_bytes Number of bytes in the view. * @throws std::out_of_range if the subrange is outside this storage. */ - auto view(std::size_t offset_bytes, std::size_t count_bytes) const { + auto view(std::size_t offset_bytes, std::size_t count_bytes) const + requires requires(const Impl& value) { + value.view_impl(std::size_t{}, std::size_t{}); + } + { return impl().view_impl(offset_bytes, count_bytes); } @@ -78,7 +167,9 @@ class StorageBase : public SerializationBase { */ void serialize_impl(BinaryWriter& writer) const { writer.write_size(size_bytes()); - writer.write_bytes(as_bytes()); + for (const std::span segment : segments()) { + writer.write_bytes(segment); + } } /** @brief Resize mutable storage to hold at least @p size_bits bits. */ @@ -124,6 +215,13 @@ class StorageBase : public SerializationBase { } private: + /** @brief Validate a logical byte range without overflowing. */ + void validate_range(position_type position, std::size_t count_bytes) const { + if (!contains(position, count_bytes)) { + throw std::out_of_range("Storage range is outside the working window"); + } + } + /** @brief Return this facade as its concrete CRTP implementation. */ const Impl& impl() const { return static_cast(*this); } diff --git a/include/pixie/storage/aligned.h b/include/pixie/storage/aligned.h index 06a8da7..f191e24 100644 --- a/include/pixie/storage/aligned.h +++ b/include/pixie/storage/aligned.h @@ -56,6 +56,12 @@ class AlignedStorage : public StorageBase { /** @brief Return the logical number of exposed bytes. */ std::size_t size_bytes_impl() const { return logical_size_bytes_; } + /** @brief Return the first logical byte position. */ + position_type begin_position_impl() const { return 0; } + + /** @brief Return the position one past the final logical byte. */ + position_type end_position_impl() const { return logical_size_bytes_; } + /** @brief Return the logical number of exposed bytes. */ std::size_t logical_size_bytes() const { return logical_size_bytes_; } @@ -81,6 +87,18 @@ class AlignedStorage : public StorageBase { .first(logical_size_bytes_); } + /** @brief Return a checked logical byte range as one physical segment. */ + SplitSpan segments_impl(std::size_t offset_bytes, + std::size_t count_bytes) const { + return SplitSpan(as_bytes_impl().subspan(offset_bytes, count_bytes)); + } + + /** @brief Return a checked logical byte range as one writable segment. */ + SplitSpan segments_impl(std::size_t offset_bytes, + std::size_t count_bytes) { + return SplitSpan(writable_bytes_impl().subspan(offset_bytes, count_bytes)); + } + /** @brief Return a checked read-only byte subrange. */ ReadOnlyStorageView view_impl(std::size_t offset_bytes, std::size_t count_bytes) const { diff --git a/include/pixie/storage/implementations.h b/include/pixie/storage/implementations.h index d438c1a..235b5a7 100644 --- a/include/pixie/storage/implementations.h +++ b/include/pixie/storage/implementations.h @@ -6,7 +6,9 @@ * * - `AlignedStorage`: owning, mutable, 64-byte-aligned storage. * - `ReadOnlyStorageView`: non-owning read-only byte storage. + * - `SlidingWindowStorage`: owning fixed-capacity sliding byte storage. */ #include #include +#include diff --git a/include/pixie/storage/read_only_view.h b/include/pixie/storage/read_only_view.h index 9ac8622..47531f0 100644 --- a/include/pixie/storage/read_only_view.h +++ b/include/pixie/storage/read_only_view.h @@ -22,9 +22,21 @@ class ReadOnlyStorageView : public StorageBase { /** @brief Return the number of viewed bytes. */ std::size_t size_bytes_impl() const { return data_.size(); } + /** @brief Return the first logical byte position. */ + position_type begin_position_impl() const { return 0; } + + /** @brief Return the position one past the final logical byte. */ + position_type end_position_impl() const { return data_.size(); } + /** @brief Return the viewed bytes. */ std::span as_bytes_impl() const { return data_; } + /** @brief Return a checked viewed byte range as one physical segment. */ + SplitSpan segments_impl(std::size_t offset_bytes, + std::size_t count_bytes) const { + return SplitSpan(data_.subspan(offset_bytes, count_bytes)); + } + /** @brief Return a checked read-only byte subrange. */ ReadOnlyStorageView view_impl(std::size_t offset_bytes, std::size_t count_bytes) const { diff --git a/include/pixie/storage/sliding_window.h b/include/pixie/storage/sliding_window.h new file mode 100644 index 0000000..18b9eef --- /dev/null +++ b/include/pixie/storage/sliding_window.h @@ -0,0 +1,157 @@ +#pragma once + +/** + * @file sliding_window.h + * @brief Fixed-capacity ring-backed sliding byte storage. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace pixie { + +/** + * @brief Owning fixed-capacity storage over a monotonically positioned window. + * + * @details The complete backing ring is allocated during construction and is + * never resized. Appending advances `end_position()` and evicts the oldest + * bytes as necessary. Mutable and const access use absolute logical positions + * and return at most two physical spans. + * + * Segments remain valid while their complete logical range remains inside the + * working window. Moving or destroying the storage invalidates all segments. + */ +class SlidingWindowStorage : public StorageBase { + public: + /** @brief Construct an empty window with immutable byte capacity. */ + explicit SlidingWindowStorage(std::size_t capacity_bytes) + : capacity_bytes_(validate_capacity(capacity_bytes)), + data_(capacity_bytes_) {} + + SlidingWindowStorage(const SlidingWindowStorage&) = default; + + /** @brief Transfer the fixed backing allocation. */ + SlidingWindowStorage(SlidingWindowStorage&& other) noexcept + : capacity_bytes_(std::exchange(other.capacity_bytes_, 0)), + data_(std::move(other.data_)), + begin_position_(std::exchange(other.begin_position_, 0)), + end_position_(std::exchange(other.end_position_, 0)) {} + + SlidingWindowStorage& operator=(const SlidingWindowStorage&) = delete; + SlidingWindowStorage& operator=(SlidingWindowStorage&&) = delete; + + /** @brief Return the immutable logical ring capacity in bytes. */ + std::size_t capacity_bytes() const { return capacity_bytes_; } + + /** @brief Return the number of bytes currently retained. */ + std::size_t size_bytes_impl() const { + return static_cast(end_position_ - begin_position_); + } + + /** @brief Return the oldest retained logical byte position. */ + position_type begin_position_impl() const { return begin_position_; } + + /** @brief Return the position one past the newest retained byte. */ + position_type end_position_impl() const { return end_position_; } + + /** @brief Return the bytes in the fixed backing allocation. */ + std::size_t allocated_bytes_impl() const { return data_.capacity(); } + + /** + * @brief Append bytes and evict the oldest bytes beyond capacity. + * @throws std::length_error if the logical position would overflow or if a + * nonempty sequence is appended to a zero-capacity window. + */ + void append(std::span bytes) { + if (bytes.empty()) { + return; + } + if (capacity_bytes_ == 0) { + throw std::length_error( + "Cannot append to a zero-capacity sliding window"); + } + if (bytes.size() > + std::numeric_limits::max() - end_position_) { + throw std::length_error("Sliding-window position overflow"); + } + + const position_type new_end = end_position_ + bytes.size(); + if (bytes.size() >= capacity_bytes_) { + const std::span suffix = bytes.last(capacity_bytes_); + const position_type suffix_position = new_end - capacity_bytes_; + copy_into_ring(suffix_position, suffix); + begin_position_ = suffix_position; + } else { + copy_into_ring(end_position_, bytes); + const position_type retained_size = + std::min(new_end, capacity_bytes_); + begin_position_ = std::max(begin_position_, new_end - retained_size); + } + end_position_ = new_end; + } + + /** @brief Return a retained logical range as writable physical segments. */ + SplitSpan segments_impl(position_type position, + std::size_t count_bytes) { + return physical_segments(position, count_bytes, + std::span(data_)); + } + + /** @brief Return a retained logical range as read-only physical segments. */ + SplitSpan segments_impl(position_type position, + std::size_t count_bytes) const { + return physical_segments(position, count_bytes, + std::span(data_)); + } + + private: + static std::size_t validate_capacity(std::size_t capacity_bytes) { + if constexpr (sizeof(std::size_t) > sizeof(position_type)) { + if (capacity_bytes > std::numeric_limits::max()) { + throw std::length_error("Sliding-window capacity is too large"); + } + } + return capacity_bytes; + } + + template + SplitSpan physical_segments(position_type position, + std::size_t count_bytes, + std::span data) const { + if (count_bytes == 0) { + return {}; + } + const std::size_t physical_begin = + static_cast(position % capacity_bytes_); + const std::size_t first_size = + std::min(count_bytes, capacity_bytes_ - physical_begin); + return SplitSpan(data.subspan(physical_begin, first_size), + data.first(count_bytes - first_size)); + } + + void copy_into_ring(position_type position, + std::span bytes) { + SplitSpan destination = + physical_segments(position, bytes.size(), std::span(data_)); + std::size_t copied = 0; + for (const std::span segment : destination) { + std::ranges::copy(bytes.subspan(copied, segment.size()), segment.begin()); + copied += segment.size(); + } + } + + std::size_t capacity_bytes_; + std::vector data_; + position_type begin_position_ = 0; + position_type end_position_ = 0; +}; + +} // namespace pixie diff --git a/src/benchmarks/serialization_benchmarks.cpp b/src/benchmarks/serialization_benchmarks.cpp index d1f7841..f384ff8 100644 --- a/src/benchmarks/serialization_benchmarks.cpp +++ b/src/benchmarks/serialization_benchmarks.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -251,6 +252,74 @@ void BM_BinaryWriterBytesSpan(benchmark::State& state) { set_throughput(state, payload_bytes); } +template +void BM_BinaryWriterBytesSplitSpanImpl(benchmark::State& state) { + const std::size_t payload_bytes = static_cast(state.range(0)); + const std::vector source = make_payload(payload_bytes); + std::vector destination(payload_bytes); + std::vector staging(kDefaultStagingBytes); + const std::size_t split_position = Split ? payload_bytes / 2 : payload_bytes; + for (auto _ : state) { + (void)_; + pixie::SpanOutputSink sink(as_writable_span(destination)); + pixie::BinaryWriter writer(sink, as_writable_span(staging)); + const pixie::SplitSpan segments( + as_const_span(source).first(split_position), + as_const_span(source).subspan(split_position)); + for (const std::span segment : segments) { + writer.write_bytes(segment); + } + writer.finish(); + benchmark::DoNotOptimize(sink.size_bytes()); + benchmark::ClobberMemory(); + } + set_throughput(state, payload_bytes); +} + +void BM_BinaryWriterBytesSplitSpanOne(benchmark::State& state) { + BM_BinaryWriterBytesSplitSpanImpl(state); +} + +void BM_BinaryWriterBytesSplitSpanTwo(benchmark::State& state) { + BM_BinaryWriterBytesSplitSpanImpl(state); +} + +void BM_SpanDescriptor(benchmark::State& state) { + const std::vector source = make_payload(64); + for (auto _ : state) { + (void)_; + const std::span segment = as_const_span(source); + benchmark::DoNotOptimize(segment.data()); + benchmark::DoNotOptimize(segment.size()); + } +} + +template +void BM_SplitSpanDescriptorImpl(benchmark::State& state) { + const std::vector source = make_payload(64); + constexpr std::size_t kSplitPosition = Split ? 32 : 64; + for (auto _ : state) { + (void)_; + const pixie::SplitSpan segments( + as_const_span(source).first(kSplitPosition), + as_const_span(source).subspan(kSplitPosition)); + std::size_t size = 0; + for (const std::span segment : segments) { + benchmark::DoNotOptimize(segment.data()); + size += segment.size(); + } + benchmark::DoNotOptimize(size); + } +} + +void BM_SplitSpanDescriptorOne(benchmark::State& state) { + BM_SplitSpanDescriptorImpl(state); +} + +void BM_SplitSpanDescriptorTwo(benchmark::State& state) { + BM_SplitSpanDescriptorImpl(state); +} + void BM_BinaryWriterBytesVector(benchmark::State& state) { const std::size_t payload_bytes = static_cast(state.range(0)); const std::vector source = make_payload(payload_bytes); @@ -544,12 +613,36 @@ BENCHMARK(BM_BinaryReaderU64) ->PIXIE_SERIALIZATION_TIMING(); BENCHMARK(BM_BinaryWriterBytesSpan) + ->Arg(1) + ->Arg(2) + ->Arg(64) ->Arg(4 * kKiB) ->Arg(1 * kMiB) ->Arg(64 * kMiB) ->ArgName("payload_bytes") ->PIXIE_SERIALIZATION_TIMING(); +BENCHMARK(BM_BinaryWriterBytesSplitSpanOne) + ->Arg(1) + ->Arg(2) + ->Arg(64) + ->Arg(4 * kKiB) + ->Arg(1 * kMiB) + ->ArgName("payload_bytes") + ->PIXIE_SERIALIZATION_TIMING(); + +BENCHMARK(BM_BinaryWriterBytesSplitSpanTwo) + ->Arg(2) + ->Arg(64) + ->Arg(4 * kKiB) + ->Arg(1 * kMiB) + ->ArgName("payload_bytes") + ->PIXIE_SERIALIZATION_TIMING(); + +BENCHMARK(BM_SpanDescriptor)->PIXIE_SERIALIZATION_TIMING(); +BENCHMARK(BM_SplitSpanDescriptorOne)->PIXIE_SERIALIZATION_TIMING(); +BENCHMARK(BM_SplitSpanDescriptorTwo)->PIXIE_SERIALIZATION_TIMING(); + BENCHMARK(BM_BinaryWriterBytesVector) ->Arg(4 * kKiB) ->Arg(1 * kMiB) diff --git a/src/tests/storage_tests.cpp b/src/tests/storage_tests.cpp index 8c29bd7..cef11d1 100644 --- a/src/tests/storage_tests.cpp +++ b/src/tests/storage_tests.cpp @@ -40,14 +40,40 @@ concept HasWritableBytes = requires(Storage value) { value.writable_bytes(); }; template concept HasResize = requires(Storage value) { value.resize(1); }; +template +concept HasMutableSegments = requires(Storage& value) { + { value.segments() } -> std::same_as>; +}; + +template +concept HasContiguousBytes = requires(const Storage& value) { + { value.as_bytes() } -> std::same_as>; +}; + +template +std::vector collect_segments( + const pixie::SplitSpan& segments) { + std::vector result; + result.reserve(segments.size()); + for (const std::span segment : segments) { + result.insert(result.end(), segment.begin(), segment.end()); + } + return result; +} + TYPED_TEST(StorageSpecificationTest, EmptyStorageHasConsistentViews) { const std::array bytes{}; auto storage = make_storage(bytes); EXPECT_TRUE(storage.empty()); EXPECT_EQ(storage.size_bytes(), 0u); EXPECT_EQ(storage.size_bits(), 0u); + EXPECT_EQ(storage.begin_position(), 0u); + EXPECT_EQ(storage.end_position(), 0u); + EXPECT_TRUE(storage.contains(0, 0)); EXPECT_TRUE(storage.as_bytes().empty()); EXPECT_TRUE(storage.view().empty()); + EXPECT_TRUE(storage.segments().empty()); + EXPECT_EQ(storage.segments().segment_count(), 0u); } TYPED_TEST(StorageSpecificationTest, SupportsByteAndNestedViews) { @@ -60,7 +86,33 @@ TYPED_TEST(StorageSpecificationTest, SupportsByteAndNestedViews) { EXPECT_EQ(middle.as_bytes()[0], std::byte{2}); EXPECT_EQ(nested.as_bytes()[0], std::byte{3}); EXPECT_EQ(nested.as_bytes()[1], std::byte{4}); + EXPECT_EQ(storage.begin_position(), 0u); + EXPECT_EQ(storage.end_position(), bytes.size()); + EXPECT_TRUE(storage.contains(2, 4)); + EXPECT_FALSE(storage.contains(7, 2)); + const auto segments = storage.segments(2, 4); + ASSERT_EQ(segments.segment_count(), 1u); + EXPECT_EQ(segments.size(), 4u); + EXPECT_TRUE(std::ranges::equal(*segments.begin(), middle.as_bytes())); EXPECT_THROW(storage.view(storage.size_bytes(), 1), std::out_of_range); + EXPECT_THROW(storage.segments(storage.size_bytes(), 1), std::out_of_range); +} + +TEST(SplitSpanTest, CanonicalizesAndIteratesTwoSegmentsInLogicalOrder) { + const std::array first = {1, 2}; + const std::array second = {3, 4, 5}; + const pixie::SplitSpan split(first, second); + + EXPECT_EQ(split.size(), 5u); + EXPECT_EQ(split.segment_count(), 2u); + auto segment = split.begin(); + EXPECT_TRUE(std::ranges::equal(*segment++, first)); + EXPECT_TRUE(std::ranges::equal(*segment++, second)); + EXPECT_EQ(segment, split.end()); + + const pixie::SplitSpan canonical({}, second); + ASSERT_EQ(canonical.segment_count(), 1u); + EXPECT_TRUE(std::ranges::equal(*canonical.begin(), second)); } TYPED_TEST(StorageSpecificationTest, ProvidesAlignedWordViewsWhenValid) { @@ -174,6 +226,24 @@ TEST(AlignedStorageTest, PadsResizesAndProvidesWritableStorage) { storage.shrink_to_fit(); } +TEST(AlignedStorageTest, MutableSegmentsModifyTheBackingStorage) { + static_assert(HasMutableSegments); + static_assert( + std::same_as< + decltype(std::declval().segments()), + pixie::SplitSpan>); + + pixie::AlignedStorage storage(4 * 8); + auto segments = storage.segments(1, 2); + ASSERT_EQ(segments.segment_count(), 1u); + (*segments.begin())[0] = std::byte{42}; + (*segments.begin())[1] = std::byte{43}; + + EXPECT_EQ(storage.as_bytes()[1], std::byte{42}); + EXPECT_EQ(storage.as_bytes()[2], std::byte{43}); + EXPECT_THROW(storage.segments(storage.size_bytes(), 1), std::out_of_range); +} + TEST(AlignedStorageTest, CopiesCompleteWordsIntoAlignedStorage) { std::array words = {1, 2, 3}; const pixie::AlignedStorage storage{std::span(words)}; @@ -189,6 +259,11 @@ TEST(AlignedStorageTest, CopiesCompleteWordsIntoAlignedStorage) { TEST(ReadOnlyStorageViewTest, MutatingOperationsAreNotAvailable) { static_assert(!HasWritableBytes); static_assert(!HasResize); + static_assert(!HasMutableSegments); + static_assert( + std::same_as< + decltype(std::declval().segments()), + pixie::SplitSpan>); } TEST(ReadOnlyStorageViewTest, DeserializeRejectsTruncatedInput) { @@ -203,6 +278,147 @@ TEST(ReadOnlyStorageViewTest, DeserializeRejectsTruncatedInput) { EXPECT_EQ(reader.position(), 0u); } +TEST(SlidingWindowStorageTest, AppendsAndExposesAbsolutePositions) { + pixie::SlidingWindowStorage storage(8); + const std::array bytes = {std::byte{1}, std::byte{2}, std::byte{3}}; + storage.append(bytes); + + EXPECT_EQ(storage.capacity_bytes(), 8u); + EXPECT_EQ(storage.allocated_bytes(), 8u); + EXPECT_EQ(storage.begin_position(), 0u); + EXPECT_EQ(storage.end_position(), 3u); + EXPECT_EQ(storage.size_bytes(), 3u); + EXPECT_TRUE(storage.contains(0, 3)); + EXPECT_TRUE(storage.contains(3, 0)); + EXPECT_FALSE(storage.contains(3, 1)); + EXPECT_EQ(collect_segments(std::as_const(storage).segments()), + std::vector(bytes.begin(), bytes.end())); +} + +TEST(SlidingWindowStorageTest, WrapsEvictsAndRejectsExpiredRanges) { + pixie::SlidingWindowStorage storage(5); + const std::array first = {std::byte{0}, std::byte{1}, std::byte{2}}; + const std::array second = {std::byte{3}, std::byte{4}, std::byte{5}, + std::byte{6}}; + storage.append(first); + storage.append(second); + + EXPECT_EQ(storage.begin_position(), 2u); + EXPECT_EQ(storage.end_position(), 7u); + EXPECT_EQ(storage.size_bytes(), 5u); + EXPECT_EQ(storage.segments().segment_count(), 2u); + EXPECT_EQ(collect_segments(std::as_const(storage).segments()), + (std::vector{std::byte{2}, std::byte{3}, std::byte{4}, std::byte{5}, + std::byte{6}})); + EXPECT_THROW(storage.segments(1, 1), std::out_of_range); + EXPECT_THROW(storage.segments(6, 2), std::out_of_range); + EXPECT_NO_THROW(storage.segments(storage.end_position(), 0)); +} + +TEST(SlidingWindowStorageTest, SupportsMutableRangesAcrossTheWrapPoint) { + pixie::SlidingWindowStorage storage(5); + const std::array bytes = {std::byte{0}, std::byte{1}, std::byte{2}, + std::byte{3}, std::byte{4}, std::byte{5}, + std::byte{6}}; + storage.append(bytes); + + auto segments = storage.segments(4, 3); + ASSERT_EQ(segments.segment_count(), 2u); + std::byte replacement = std::byte{40}; + for (const std::span segment : segments) { + for (std::byte& value : segment) { + value = replacement; + replacement = + static_cast(static_cast(replacement) + 10); + } + } + + EXPECT_EQ(collect_segments(std::as_const(storage).segments()), + (std::vector{std::byte{2}, std::byte{3}, std::byte{40}, + std::byte{50}, std::byte{60}})); +} + +TEST(SlidingWindowStorageTest, RetainedSegmentsKeepTheirAddresses) { + pixie::SlidingWindowStorage storage(6); + const std::array initial = {std::byte{0}, std::byte{1}, std::byte{2}, + std::byte{3}, std::byte{4}, std::byte{5}}; + storage.append(initial); + const auto retained = std::as_const(storage).segments(1, 2); + const std::byte* const address = retained.begin()->data(); + + const std::array next = {std::byte{6}}; + storage.append(next); + + ASSERT_TRUE(storage.contains(1, 2)); + EXPECT_EQ(retained.begin()->data(), address); + EXPECT_EQ((*retained.begin())[0], std::byte{1}); + EXPECT_EQ((*retained.begin())[1], std::byte{2}); +} + +TEST(SlidingWindowStorageTest, OversizedAppendRetainsOnlyItsSuffix) { + pixie::SlidingWindowStorage storage(4); + const std::array bytes = {std::byte{0}, std::byte{1}, std::byte{2}, + std::byte{3}, std::byte{4}, std::byte{5}}; + storage.append(bytes); + + EXPECT_EQ(storage.begin_position(), 2u); + EXPECT_EQ(storage.end_position(), 6u); + EXPECT_EQ( + collect_segments(std::as_const(storage).segments()), + (std::vector{std::byte{2}, std::byte{3}, std::byte{4}, std::byte{5}})); +} + +TEST(SlidingWindowStorageTest, SerializesRetainedBytesInLogicalOrder) { + pixie::SlidingWindowStorage storage(4); + const std::array bytes = {std::byte{0}, std::byte{1}, std::byte{2}, + std::byte{3}, std::byte{4}, std::byte{5}}; + storage.append(bytes); + pixie::VectorOutputSink output; + pixie::BinaryWriter writer(output); + storage.serialize(writer); + writer.finish(); + + const std::vector artifact = output.take(); + pixie::BinaryReader reader(artifact); + EXPECT_EQ(reader.read_size(), 4u); + EXPECT_TRUE(std::ranges::equal( + reader.read_bytes(4), + std::array{std::byte{2}, std::byte{3}, std::byte{4}, std::byte{5}})); + EXPECT_TRUE(reader.empty()); +} + +TEST(SlidingWindowStorageTest, HasFixedCapacityAndIndependentCopies) { + static_assert(pixie::StorageImplementation); + static_assert(HasMutableSegments); + static_assert(!HasContiguousBytes); + static_assert(!HasWritableBytes); + static_assert(!HasResize); + static_assert(std::is_copy_constructible_v); + static_assert(std::is_move_constructible_v); + static_assert(!std::is_copy_assignable_v); + static_assert(!std::is_move_assignable_v); + + pixie::SlidingWindowStorage original(3); + const std::array bytes = {std::byte{1}, std::byte{2}}; + original.append(bytes); + pixie::SlidingWindowStorage copy(original); + (*copy.segments(0, 1).begin())[0] = std::byte{9}; + + EXPECT_EQ((*std::as_const(original).segments(0, 1).begin())[0], std::byte{1}); + EXPECT_EQ((*std::as_const(copy).segments(0, 1).begin())[0], std::byte{9}); +} + +TEST(SlidingWindowStorageTest, ZeroCapacityAcceptsOnlyEmptyAppends) { + pixie::SlidingWindowStorage storage(0); + const std::array empty{}; + storage.append(empty); + EXPECT_TRUE(storage.empty()); + EXPECT_TRUE(storage.segments().empty()); + + const std::array nonempty = {std::byte{1}}; + EXPECT_THROW(storage.append(nonempty), std::length_error); +} + TEST(MappedFileTest, MapsContentsAndIsMoveOnly) { static_assert(!std::is_copy_constructible_v); const auto path = From 43a84409b2f0ca59c25dcc9ed255a974fbee427b Mon Sep 17 00:00:00 2001 From: Nikolay Malkovsky Date: Tue, 1 Sep 2026 00:21:43 +0300 Subject: [PATCH 2/3] Refine sliding-window range extension --- include/pixie/storage.h | 29 +++++++++- include/pixie/storage/sliding_window.h | 78 ++++++++++++++----------- src/tests/storage_tests.cpp | 79 ++++++++++++++++++++------ 3 files changed, 135 insertions(+), 51 deletions(-) diff --git a/include/pixie/storage.h b/include/pixie/storage.h index 50ea0f2..2977909 100644 --- a/include/pixie/storage.h +++ b/include/pixie/storage.h @@ -22,6 +22,25 @@ namespace pixie { /** * @brief CRTP facade for byte-addressable storage. * + * @details `Impl` must provide `size_bytes_impl()`, + * `begin_position_impl()`, `end_position_impl()`, and the const overload of + * `segments_impl(position, count_bytes)`. The positions describe one absolute + * half-open logical range `[begin_position_impl(), end_position_impl())` and + * must satisfy `end_position_impl() - begin_position_impl() == + * size_bytes_impl()`. + * + * `segments_impl()` is called only with a range contained in that logical + * range. It must return the requested bytes in logical order, split into at + * most two physical spans whose combined size is exactly `count_bytes`. + * Returned spans borrow the implementation's backing storage and remain valid + * according to that storage's documented lifetime and invalidation rules. + * + * Mutable implementations may provide a writable `segments_impl()` overload. + * They may also provide `prepare_segments_impl(position, count_bytes)` to make + * a future writable range available before validation. This preparation hook + * must either make the complete range available or throw without changing the + * implementation. + * * @tparam Impl Concrete storage implementation. */ template @@ -90,9 +109,12 @@ class StorageBase : public SerializationBase { /** * @brief Return a checked writable logical byte range as one or two spans. + * @details An implementation with a preparation hook may make a future range + * available before checking it. Any newly exposed bytes remain the caller's + * responsibility to initialize. * @param position First logical byte position in the range. * @param count_bytes Number of logical bytes in the range. - * @throws std::out_of_range if the range is outside this storage. + * @throws std::out_of_range if the range cannot be made available. */ SplitSpan segments(position_type position, std::size_t count_bytes) requires requires(Impl& value) { @@ -101,6 +123,11 @@ class StorageBase : public SerializationBase { } -> std::same_as>; } { + if constexpr (requires(Impl& value) { + value.prepare_segments_impl(position_type{}, std::size_t{}); + }) { + impl().prepare_segments_impl(position, count_bytes); + } validate_range(position, count_bytes); return impl().segments_impl(position, count_bytes); } diff --git a/include/pixie/storage/sliding_window.h b/include/pixie/storage/sliding_window.h index 18b9eef..5456804 100644 --- a/include/pixie/storage/sliding_window.h +++ b/include/pixie/storage/sliding_window.h @@ -22,9 +22,12 @@ namespace pixie { * @brief Owning fixed-capacity storage over a monotonically positioned window. * * @details The complete backing ring is allocated during construction and is - * never resized. Appending advances `end_position()` and evicts the oldest - * bytes as necessary. Mutable and const access use absolute logical positions - * and return at most two physical spans. + * never resized. Extending advances `end_position()` and evicts the oldest + * bytes as necessary without initializing the newly exposed storage. Mutable + * and const access use absolute logical positions and return at most two + * physical spans. Requesting a future mutable range extends the window through + * that range automatically; the caller remains responsible for assigning its + * contents. * * Segments remain valid while their complete logical range remains inside the * working window. Moving or destroying the storage invalidates all segments. @@ -66,38 +69,58 @@ class SlidingWindowStorage : public StorageBase { std::size_t allocated_bytes_impl() const { return data_.capacity(); } /** - * @brief Append bytes and evict the oldest bytes beyond capacity. + * @brief Advance the logical end by a number of bytes. + * @details Newly exposed bytes are not initialized. Advancing beyond the + * capacity evicts the oldest logical bytes while retaining the fixed backing + * allocation. * @throws std::length_error if the logical position would overflow or if a - * nonempty sequence is appended to a zero-capacity window. + * nonzero extension is requested for a zero-capacity window. */ - void append(std::span bytes) { - if (bytes.empty()) { + void extend(std::size_t count_bytes) { + if (count_bytes == 0) { return; } if (capacity_bytes_ == 0) { - throw std::length_error( - "Cannot append to a zero-capacity sliding window"); + throw std::length_error("Cannot extend a zero-capacity sliding window"); } - if (bytes.size() > + if (count_bytes > std::numeric_limits::max() - end_position_) { throw std::length_error("Sliding-window position overflow"); } - const position_type new_end = end_position_ + bytes.size(); - if (bytes.size() >= capacity_bytes_) { - const std::span suffix = bytes.last(capacity_bytes_); - const position_type suffix_position = new_end - capacity_bytes_; - copy_into_ring(suffix_position, suffix); - begin_position_ = suffix_position; - } else { - copy_into_ring(end_position_, bytes); - const position_type retained_size = - std::min(new_end, capacity_bytes_); - begin_position_ = std::max(begin_position_, new_end - retained_size); - } + const position_type new_end = end_position_ + count_bytes; + begin_position_ = new_end > capacity_bytes_ ? new_end - capacity_bytes_ : 0; end_position_ = new_end; } + /** + * @brief Extend through a requested future writable range when necessary. + * @throws std::out_of_range if the requested range cannot fit in the window. + * @throws std::length_error if the requested logical range overflows. + */ + void prepare_segments_impl(position_type position, std::size_t count_bytes) { + if (count_bytes == 0 || (position <= end_position_ && + count_bytes <= end_position_ - position)) { + return; + } + if (count_bytes > std::numeric_limits::max() - position) { + throw std::length_error("Sliding-window range overflow"); + } + + const position_type requested_end = position + count_bytes; + if (count_bytes > capacity_bytes_ || position < begin_position_) { + throw std::out_of_range( + "Storage range cannot fit inside the working window"); + } + const position_type extension = requested_end - end_position_; + if constexpr (sizeof(position_type) > sizeof(std::size_t)) { + if (extension > std::numeric_limits::max()) { + throw std::length_error("Sliding-window extension is too large"); + } + } + extend(static_cast(extension)); + } + /** @brief Return a retained logical range as writable physical segments. */ SplitSpan segments_impl(position_type position, std::size_t count_bytes) { @@ -137,17 +160,6 @@ class SlidingWindowStorage : public StorageBase { data.first(count_bytes - first_size)); } - void copy_into_ring(position_type position, - std::span bytes) { - SplitSpan destination = - physical_segments(position, bytes.size(), std::span(data_)); - std::size_t copied = 0; - for (const std::span segment : destination) { - std::ranges::copy(bytes.subspan(copied, segment.size()), segment.begin()); - copied += segment.size(); - } - } - std::size_t capacity_bytes_; std::vector data_; position_type begin_position_ = 0; diff --git a/src/tests/storage_tests.cpp b/src/tests/storage_tests.cpp index cef11d1..f644358 100644 --- a/src/tests/storage_tests.cpp +++ b/src/tests/storage_tests.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -61,6 +62,18 @@ std::vector collect_segments( return result; } +void assign_segments(pixie::SlidingWindowStorage& storage, + pixie::SlidingWindowStorage::position_type position, + std::span bytes) { + const pixie::SplitSpan destination = + storage.segments(position, bytes.size()); + std::size_t copied = 0; + for (const std::span segment : destination) { + std::ranges::copy(bytes.subspan(copied, segment.size()), segment.begin()); + copied += segment.size(); + } +} + TYPED_TEST(StorageSpecificationTest, EmptyStorageHasConsistentViews) { const std::array bytes{}; auto storage = make_storage(bytes); @@ -278,10 +291,11 @@ TEST(ReadOnlyStorageViewTest, DeserializeRejectsTruncatedInput) { EXPECT_EQ(reader.position(), 0u); } -TEST(SlidingWindowStorageTest, AppendsAndExposesAbsolutePositions) { +TEST(SlidingWindowStorageTest, ExtendsAndExposesAbsolutePositions) { pixie::SlidingWindowStorage storage(8); const std::array bytes = {std::byte{1}, std::byte{2}, std::byte{3}}; - storage.append(bytes); + storage.extend(bytes.size()); + assign_segments(storage, 0, bytes); EXPECT_EQ(storage.capacity_bytes(), 8u); EXPECT_EQ(storage.allocated_bytes(), 8u); @@ -300,8 +314,8 @@ TEST(SlidingWindowStorageTest, WrapsEvictsAndRejectsExpiredRanges) { const std::array first = {std::byte{0}, std::byte{1}, std::byte{2}}; const std::array second = {std::byte{3}, std::byte{4}, std::byte{5}, std::byte{6}}; - storage.append(first); - storage.append(second); + assign_segments(storage, 0, first); + assign_segments(storage, first.size(), second); EXPECT_EQ(storage.begin_position(), 2u); EXPECT_EQ(storage.end_position(), 7u); @@ -311,16 +325,46 @@ TEST(SlidingWindowStorageTest, WrapsEvictsAndRejectsExpiredRanges) { (std::vector{std::byte{2}, std::byte{3}, std::byte{4}, std::byte{5}, std::byte{6}})); EXPECT_THROW(storage.segments(1, 1), std::out_of_range); - EXPECT_THROW(storage.segments(6, 2), std::out_of_range); + EXPECT_THROW(std::as_const(storage).segments(6, 2), std::out_of_range); EXPECT_NO_THROW(storage.segments(storage.end_position(), 0)); } +TEST(SlidingWindowStorageTest, FutureWritableRangeExtendsAutomatically) { + pixie::SlidingWindowStorage storage(5); + const std::array bytes = {std::byte{3}, std::byte{4}, std::byte{5}}; + assign_segments(storage, 2, bytes); + + EXPECT_EQ(storage.begin_position(), 0u); + EXPECT_EQ(storage.end_position(), 5u); + EXPECT_EQ(collect_segments(std::as_const(storage).segments(2, 3)), + std::vector(bytes.begin(), bytes.end())); +} + +TEST(SlidingWindowStorageTest, InvalidFutureRangeDoesNotExtend) { + pixie::SlidingWindowStorage storage(4); + storage.extend(2); + + EXPECT_THROW(storage.segments(2, 5), std::out_of_range); + EXPECT_EQ(storage.begin_position(), 0u); + EXPECT_EQ(storage.end_position(), 2u); +} + +TEST(SlidingWindowStorageTest, OverflowingFutureRangeDoesNotExtend) { + pixie::SlidingWindowStorage storage(4); + constexpr auto max_position = + std::numeric_limits::max(); + + EXPECT_THROW(storage.segments(max_position - 1, 3), std::length_error); + EXPECT_EQ(storage.begin_position(), 0u); + EXPECT_EQ(storage.end_position(), 0u); +} + TEST(SlidingWindowStorageTest, SupportsMutableRangesAcrossTheWrapPoint) { pixie::SlidingWindowStorage storage(5); const std::array bytes = {std::byte{0}, std::byte{1}, std::byte{2}, std::byte{3}, std::byte{4}, std::byte{5}, std::byte{6}}; - storage.append(bytes); + assign_segments(storage, 2, std::span(bytes).subspan(2)); auto segments = storage.segments(4, 3); ASSERT_EQ(segments.segment_count(), 2u); @@ -342,12 +386,13 @@ TEST(SlidingWindowStorageTest, RetainedSegmentsKeepTheirAddresses) { pixie::SlidingWindowStorage storage(6); const std::array initial = {std::byte{0}, std::byte{1}, std::byte{2}, std::byte{3}, std::byte{4}, std::byte{5}}; - storage.append(initial); + assign_segments(storage, 0, initial); const auto retained = std::as_const(storage).segments(1, 2); const std::byte* const address = retained.begin()->data(); const std::array next = {std::byte{6}}; - storage.append(next); + storage.extend(next.size()); + assign_segments(storage, 6, next); ASSERT_TRUE(storage.contains(1, 2)); EXPECT_EQ(retained.begin()->data(), address); @@ -355,11 +400,12 @@ TEST(SlidingWindowStorageTest, RetainedSegmentsKeepTheirAddresses) { EXPECT_EQ((*retained.begin())[1], std::byte{2}); } -TEST(SlidingWindowStorageTest, OversizedAppendRetainsOnlyItsSuffix) { +TEST(SlidingWindowStorageTest, ExtensionBeyondCapacityRetainsNewestPositions) { pixie::SlidingWindowStorage storage(4); const std::array bytes = {std::byte{0}, std::byte{1}, std::byte{2}, std::byte{3}, std::byte{4}, std::byte{5}}; - storage.append(bytes); + storage.extend(bytes.size()); + assign_segments(storage, 2, std::span(bytes).subspan(2)); EXPECT_EQ(storage.begin_position(), 2u); EXPECT_EQ(storage.end_position(), 6u); @@ -372,7 +418,8 @@ TEST(SlidingWindowStorageTest, SerializesRetainedBytesInLogicalOrder) { pixie::SlidingWindowStorage storage(4); const std::array bytes = {std::byte{0}, std::byte{1}, std::byte{2}, std::byte{3}, std::byte{4}, std::byte{5}}; - storage.append(bytes); + storage.extend(bytes.size()); + assign_segments(storage, 2, std::span(bytes).subspan(2)); pixie::VectorOutputSink output; pixie::BinaryWriter writer(output); storage.serialize(writer); @@ -400,7 +447,7 @@ TEST(SlidingWindowStorageTest, HasFixedCapacityAndIndependentCopies) { pixie::SlidingWindowStorage original(3); const std::array bytes = {std::byte{1}, std::byte{2}}; - original.append(bytes); + assign_segments(original, 0, bytes); pixie::SlidingWindowStorage copy(original); (*copy.segments(0, 1).begin())[0] = std::byte{9}; @@ -408,15 +455,13 @@ TEST(SlidingWindowStorageTest, HasFixedCapacityAndIndependentCopies) { EXPECT_EQ((*std::as_const(copy).segments(0, 1).begin())[0], std::byte{9}); } -TEST(SlidingWindowStorageTest, ZeroCapacityAcceptsOnlyEmptyAppends) { +TEST(SlidingWindowStorageTest, ZeroCapacityAcceptsOnlyEmptyExtensions) { pixie::SlidingWindowStorage storage(0); - const std::array empty{}; - storage.append(empty); + storage.extend(0); EXPECT_TRUE(storage.empty()); EXPECT_TRUE(storage.segments().empty()); - const std::array nonempty = {std::byte{1}}; - EXPECT_THROW(storage.append(nonempty), std::length_error); + EXPECT_THROW(storage.extend(1), std::length_error); } TEST(MappedFileTest, MapsContentsAndIsMoveOnly) { From c18cf17cdebdd6e5edc0aa59dbbc7c813778bdcf Mon Sep 17 00:00:00 2001 From: Nikolay Malkovsky Date: Thu, 3 Sep 2026 00:15:11 +0300 Subject: [PATCH 3/3] Storage api requirements update --- include/pixie/storage.h | 50 ++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/include/pixie/storage.h b/include/pixie/storage.h index 2977909..5992f80 100644 --- a/include/pixie/storage.h +++ b/include/pixie/storage.h @@ -22,24 +22,42 @@ namespace pixie { /** * @brief CRTP facade for byte-addressable storage. * - * @details `Impl` must provide `size_bytes_impl()`, - * `begin_position_impl()`, `end_position_impl()`, and the const overload of - * `segments_impl(position, count_bytes)`. The positions describe one absolute - * half-open logical range `[begin_position_impl(), end_position_impl())` and - * must satisfy `end_position_impl() - begin_position_impl() == - * size_bytes_impl()`. + * @details `Impl` must provide `size_bytes_impl()` and the following required + * extension points. * - * `segments_impl()` is called only with a range contained in that logical - * range. It must return the requested bytes in logical order, split into at - * most two physical spans whose combined size is exactly `count_bytes`. - * Returned spans borrow the implementation's backing storage and remain valid - * according to that storage's documented lifetime and invalidation rules. + * @par Required: `begin_position_impl() const` + * Returns the absolute logical position of the first exposed byte as a + * `position_type`. The result may advance when an implementation evicts bytes, + * but positions of retained bytes do not change. * - * Mutable implementations may provide a writable `segments_impl()` overload. - * They may also provide `prepare_segments_impl(position, count_bytes)` to make - * a future writable range available before validation. This preparation hook - * must either make the complete range available or throw without changing the - * implementation. + * @par Required: `end_position_impl() const` + * Returns the absolute logical position one past the last exposed byte as a + * `position_type`. It must be at least `begin_position_impl()`, and their + * difference must be representable by `std::size_t` and equal + * `size_bytes_impl()`. + * + * @par Required: `segments_impl(position, count_bytes) const` + * Accepts a `position_type` and a `std::size_t` and returns + * `SplitSpan` containing exactly the bytes in `[position, + * position + count_bytes)`. The physical spans are ordered by logical position + * and their combined size equals `count_bytes`. Before calling this hook, the + * facade verifies without overflow that `position` is in the closed range from + * `begin_position_impl()` through `end_position_impl()` and that `count_bytes + * <= end_position_impl() - position`; an empty range at the end position is + * valid. The returned spans borrow the implementation's backing storage. The + * implementation must document the backing storage's ownership requirements + * and the operations that invalidate its spans. + * + * @par Optional: `segments_impl(position, count_bytes)` + * A mutable implementation may provide this extension point to enable + * writable segment access. It accepts the same parameter types as the const + * overload and returns `SplitSpan`. It has the same range, ordering, + * size, lifetime, and invalidation contract as the const overload, and writes + * through the returned spans modify the corresponding logical bytes. If the + * implementation also provides `prepare_segments_impl(position, count_bytes)`, + * the facade calls that hook before validating the range. The preparation hook + * must make the complete requested range available or throw without changing + * the implementation. * * @tparam Impl Concrete storage implementation. */