diff --git a/.cspell.json b/.cspell.json index 2a952a4..e38940c 100644 --- a/.cspell.json +++ b/.cspell.json @@ -4,6 +4,9 @@ "words": [ "scatter", "span", - "libhal" + "libhal", + "spanable", + "subssp" ] } + diff --git a/.gitignore b/.gitignore index 801532f..7a8ee4e 100644 --- a/.gitignore +++ b/.gitignore @@ -65,4 +65,4 @@ CMakeUserPresets.json .vscode/launch.json # Allowed for clangd resolver script -!.vscode/settings.json \ No newline at end of file +!.vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json index 5549e99..fcc6283 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { "clangd.path": "./compile_commands_clangd_resolver.py" -} \ No newline at end of file +} + diff --git a/CMakeLists.txt b/CMakeLists.txt index 7a23711..323931a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,6 +24,7 @@ libhal_project_init() libhal_add_library(scatter-span MODULES modules/scatter_span.cppm + modules/core.cppm ) libhal_apply_compile_options(scatter-span) diff --git a/conanfile.py b/conanfile.py index c9d6da7..89b9ece 100644 --- a/conanfile.py +++ b/conanfile.py @@ -97,7 +97,7 @@ def generate(self): def build(self): cmake = CMake(self) - cmake.configure() + cmake.configure(cli_args=["-Wno-dev"]) cmake.build() if not self.conf.get("tools.build:skip_test", default=False): cmake.ctest(["--output-on-failure"]) diff --git a/modules/core.cppm b/modules/core.cppm new file mode 100644 index 0000000..4f27dda --- /dev/null +++ b/modules/core.cppm @@ -0,0 +1,537 @@ +module; + +#include +#include +#include +#include + +export module scatter_span:core; + +namespace mem::detail { +template +struct scatter_array_storage +{ + std::array, N> m_internal_arr; +}; +}; // namespace mem::detail + +export namespace mem { + +template +class scatter_span; + +/** + * @brief Forward iterator over the constituent spans of a scatter_span. + * + * @details Each dereference yields a @c std::span representing one + * contiguous segment. + * + * + * The cached view (@c m_subspan_cache) is recomputed on every pointer move so + * that @c operator* and @c operator-> are O(1). + * + * @tparam T Element type of the underlying spans (accessed as @c T const). + */ +template +struct scatter_span_iterator +{ + using iterator_category = std::forward_iterator_tag; + using value_type = std::span; + using difference_type = std::ptrdiff_t; + using pointer = value_type const*; + using reference = value_type const&; + + /** + * @brief Recomputes the cached trimmed view for the span at @c m_ptr. + * + * @details No-ops when @c m_ptr is past the last span (end sentinel). + * Otherwise applies the appropriate trim rule based on whether the current + * span is the first, last, both, or neither. + */ + constexpr void update_cache() + { + auto const* first = m_ssp->m_spans.data(); + auto const* last = m_ssp->m_spans.data() + m_ssp->m_spans.size() - 1; + if (m_ptr > last) { + return; + } else if (first == last) { + m_subspan_cache = m_ptr->subspan(m_ssp->m_start_pos, m_ssp->m_final_len); + } else if (m_ptr == first) { + m_subspan_cache = m_ptr->subspan(m_ssp->m_start_pos); + } else if (m_ptr == last) { + m_subspan_cache = m_ptr->first(m_ssp->m_final_len); + } else { + m_subspan_cache = *m_ptr; + } + } + + /** + * @brief Returns the trimmed span view for the current position. + * @return A const reference to the cached @c std::span. + */ + constexpr reference operator*() const + { + return m_subspan_cache; + } + + /** + * @brief Returns a pointer to the trimmed span view for the current position. + * @return A pointer to the cached @c std::span. + */ + constexpr pointer operator->() const + { + return &m_subspan_cache; + } + + /** + * @brief Prefix increment. Advances to the next span and updates the cache. + * @return Reference to @c *this after advancing. + */ + constexpr scatter_span_iterator& operator++() + { + ++m_ptr; + update_cache(); + return *this; + } + + /** + * @brief Postfix increment. Returns a copy of the iterator before advancing. + * @return Copy of @c *this prior to the increment. + */ + constexpr scatter_span_iterator operator++(int) + { + auto tmp = *this; + ++(*this); + return tmp; + } + + /** + * @brief Prefix decrement. Retreats to the previous span and updates the + * cache. + * @return Reference to @c *this after retreating. + */ + constexpr scatter_span_iterator& operator--() + { + --m_ptr; + update_cache(); + return *this; + } + + /** + * @brief Postfix decrement. Returns a copy of the iterator before retreating. + * @return Copy of @c *this prior to the decrement. + */ + constexpr scatter_span_iterator operator--(int) + { + auto tmp = *this; + --(*this); + return tmp; + } + + /** + * @brief Returns a new iterator advanced by @p n spans. + * @param n Number of spans to advance. + * @return New iterator pointing @p n spans ahead. + */ + constexpr scatter_span_iterator operator+(difference_type n) const + { + auto tmp = *this; + tmp.m_ptr += n; + tmp.update_cache(); + return tmp; + } + + /** + * @brief Returns a new iterator retreated by @p n spans. + * @param n Number of spans to retreat. + * @return New iterator pointing @p n spans behind. + */ + constexpr scatter_span_iterator operator-(difference_type n) const + { + auto tmp = *this; + tmp.m_ptr -= n; + tmp.update_cache(); + return tmp; + } + + /** + * @brief Returns the signed span distance between two iterators. + * @param other The iterator to subtract. + * @return Number of spans from @p other to @c *this. + */ + constexpr difference_type operator-(scatter_span_iterator const& other) const + { + return m_ptr - other.m_ptr; + } + + /** + * @brief Commutative addition: advances @p it by @p n spans. + * @param n Number of spans to advance. + * @param it Iterator to advance. + * @return New iterator pointing @p n spans ahead of @p it. + */ + friend constexpr scatter_span_iterator operator+( + difference_type n, + scatter_span_iterator const& it) + { + return it + n; + } + + /** + * @brief Equality comparison by underlying pointer. + * @param other Iterator to compare against. + * @return @c true if both iterators point to the same span. + */ + constexpr bool operator==(scatter_span_iterator const& other) const + { + return m_ptr == other.m_ptr; + } + + /** + * @brief Inequality comparison by underlying pointer. + * @param other Iterator to compare against. + * @return @c true if the iterators point to different spans. + */ + constexpr bool operator!=(scatter_span_iterator const& other) const + { + return m_ptr != other.m_ptr; + }; + + /** + * @brief Constructs an iterator for @p p_ssp pointing at @p p_ptr. + * @param p_ssp Parent scatter_span providing trim metadata. + * @param p_ptr Pointer into @p p_ssp's internal span array. + * @note The cache is primed immediately on construction. + */ + constexpr explicit scatter_span_iterator(scatter_span const& p_ssp, + pointer p_ptr) + : m_ssp(&p_ssp) + , m_ptr(p_ptr) + { + update_cache(); + } + + scatter_span const* + m_ssp; ///< Parent scatter_span; provides trim metadata. + pointer m_ptr; ///< Pointer into m_ssp->m_spans. + std::span + m_subspan_cache; ///< Cached trimmed view for the current span. +}; + +/** + @brief A concept for types that can be in some way, converted to a @c + std::span. Used primarily to denote container types. + + @tparam T - the underlying type for a given spanable container. + */ +template +concept spanable = requires(T& t) { + { std::span(t) }; +}; + +/** + * @brief Non-owning immutable view over a collection of disjoint memory spans. + * + * @details Works similarly to @c std::span, but the viewed elements do not + * need to be contiguous in memory. Each constituent chunk is a separate + * @c std::span. Iterating yields each chunk a span in order. + * + * Prefer @c scatter_span for ad-hoc, call-site construction. Commonly used for + * when forwarding discontiguous buffers to an API without a heap allocation or + * memcpy. Prefer @c scatter_array when the span set is reused or must outlive + * the call site. + * + * @par Example + * @code + * void send_packet(mem::scatter_span payload); + * + * std::array header = { ... }; + * std::array body = { ... }; + * std::array crc = { ... }; + * + * // Three discontiguous buffers assembled into one logical packet inline, + * // with no heap allocation or memcpy: + * send_packet({ header, body, crc }); + * @endcode + * + * @tparam T Element type. Elements are always accessed as @c T const. + */ +template +class scatter_span +{ + +public: + /** + * @brief Constructs a scatter_span from a brace-enclosed list of spans. + * + * @details The first and last spans are recorded for trim purposes. An empty + * initializer list produces a zero-length scatter_span. + * + * @param p_il Initializer list of @c std::span chunks, in logical + * order. + */ + constexpr scatter_span(std::initializer_list> p_il) + : m_spans(p_il.begin(), p_il.size()) + , m_start_pos(0) + , m_final_len(p_il.size() == 0 ? 0 : (p_il.end() - 1)->size()) + { + } + + /** + * @brief Returns an iterator to the first span chunk. + * @return @c scatter_span_iterator pointing at the first chunk. + */ + [[nodiscard]] constexpr scatter_span_iterator begin() const + { + return scatter_span_iterator(*this, m_spans.data()); + } + + /** + * @brief Returns a past-the-end iterator. + * @return @c scatter_span_iterator pointing one past the last chunk. + */ + [[nodiscard]] constexpr scatter_span_iterator end() const + { + return scatter_span_iterator(*this, m_spans.data() + m_spans.size()); + } + + /** + * @brief Arguments for @c sub_scatter_span(). + * Offset - Logical element index to start from. + * Count - Number of elements to include. (Defaults to all remaining) + */ + struct sub_scatter_span_args + { + size_t offset = 0; + size_t count = std::dynamic_extent; + }; + + /** + * @brief Returns a sub-view of this scatter_span starting at a logical + * element offset. + * + * @details Analogous to @c std::span::subspan. No data is copied; the + * returned @c scatter_span references the same underlying memory with + * adjusted trim values. If @p p_args.offset is at or beyond the total + * length, an empty @c scatter_span is returned. If @p p_args.count exceeds + * the remaining elements it is clamped to the available length. + * + * @par Example + * @code + * std::array a = { 1, 2, 3 }; + * std::array b = { 4, 5 }; + * std::array c = { 6, 7, 8, 9 }; + * mem::scatter_array ssa(a, b, c); // length 9 + * + * // Logical elements [2, 7) → { 3, 4, 5, 6, 7 } + * auto sub = ssa.sub_scatter_span({ .offset = 2, .count = 5 }); + * @endcode + * + * @param p_args Offset and count describing the desired sub-range. + * @return A new @c scatter_span covering the requested elements. + */ + constexpr scatter_span sub_scatter_span(sub_scatter_span_args p_args) + { + auto len = length(); + + if (p_args.offset >= len) { + return scatter_span({}); + } + + if (p_args.count >= len - p_args.offset) { + p_args.count = len - p_args.offset; + } + + size_t cur_len = 0; + size_t starting_span_idx = 0; + size_t effective_span_size = 0; + bool boundary = false; + for (auto s : *this) { + effective_span_size = s.size(); + cur_len += effective_span_size; + if (cur_len > p_args.offset) { + break; + } else if (cur_len == p_args.offset) { + starting_span_idx += 1; + boundary = true; + break; + } + + starting_span_idx += 1; + } + + size_t start_pos = + boundary ? 0 : p_args.offset - (cur_len - effective_span_size); + size_t span_idx = 0; + cur_len = 0; + auto considered_spans = m_spans.subspan(starting_span_idx); + size_t adjusted_count = p_args.count + start_pos; + for (std::span s : considered_spans) { + cur_len += s.size(); + if (cur_len >= adjusted_count) { + break; + } + span_idx += 1; + } + + if (cur_len == adjusted_count) { + size_t final_len = + (span_idx == 0) ? p_args.count : considered_spans[span_idx].size(); + return scatter_span({ .start_pos = start_pos, .final_len = final_len }, + considered_spans.subspan(0, span_idx + 1)); + } + + auto final_offset = + adjusted_count - (cur_len - considered_spans[span_idx].size()); + return scatter_span( + { .start_pos = start_pos, .final_len = final_offset }, + considered_spans.subspan(0, span_idx + 1)); + } + + /** + * @brief Returns the total number of logical elements across all chunks. + * + * @return Total element count. + */ + [[nodiscard]] size_t length() const + { + if (m_spans.size() <= 1) { + return m_final_len; + } + + size_t res = m_spans[0].size() - m_start_pos; + + for (auto const& s : m_spans.subspan(1, m_spans.size() - 2)) { + res += s.size(); + } + + return res + m_final_len; + } + + friend struct scatter_span_iterator; + +protected: + /** + * @brief Constructs a scatter_span directly from a span-of-spans. + * + * @details Used by @c scatter_array to hand its internal storage to the base + * class. Assumes full extents of the first and last chunks with no trimming. + * + * @param p_spans View over the array of chunk spans. + */ + constexpr scatter_span(std::span const> p_spans) + : m_spans(p_spans) + , m_start_pos(0) + , m_final_len((p_spans.end() - 1)->size()) + { + } + + std::span const> m_spans; + size_t m_start_pos; + size_t m_final_len; + + /** + * @brief Trim metadata produced by @c sub_scatter_span(). + */ + struct position_data + { + size_t start_pos = + 0; ///< Index into the first chunk to begin reading from. + size_t final_len = 0; ///< Number of elements to read from the last chunk. + }; + + /** + * @brief Constructs a trimmed scatter_span with explicit position metadata. + * + * @details Used internally by @c sub_scatter_span() to produce a sub-view + * without copying data. + * + * @param p_pos Trim parameters for the first and last chunks. + * @param p_spans The subset of chunk spans to reference. + */ + constexpr scatter_span(position_data p_pos, + std::span const> p_spans) + : m_spans(p_spans) + , m_start_pos(p_pos.start_pos) + , m_final_len(p_pos.final_len) + { + } +}; + +/** + * @brief Owning scatter span. Stores the chunk span array internally and + * exposes the full @c scatter_span interface. + * + * @details Unlike @c scatter_span, @c scatter_array owns the @c std::span + * descriptors (not the underlying data). This makes it safe to store as a + * member or return from a function without dangling. The number of chunks @p N + * is fixed at compile time; the backing arrays themselves remain external and + * must outlive the @c scatter_array. + * + * Prefer @c scatter_array when the same set of chunks is iterated or + * sub-spanned multiple times across a lifetime (e.g. a reusable packet + * descriptor). Prefer @c scatter_span for single-use, call-site construction. + * + * @par Example + * @code + * std::array first = { 1, 2, 3 }; + * std::array second = { 4, 5 }; + * std::array third = { 6, 7, 8, 9 }; + * + * // N deduced as 3 via CTAD + * mem::scatter_array ssa(first, second, third); + * + * // Reuse: take different sub-views without re-specifying chunks each time + * auto head = ssa.sub_scatter_span({ .count = 5 }); // { 1,2,3,4,5 } + * auto tail = ssa.sub_scatter_span({ .offset = 5 }); // { 6,7,8,9 } + * auto mid = ssa.sub_scatter_span({ .offset = 2, .count = 5 }); // { 3,4,5,6,7 + * } + * @endcode + * + * @tparam T Element type. + * @tparam N Number of chunk spans stored internally. + */ +template +class scatter_array + : private detail::scatter_array_storage + , public scatter_span +{ +public: + /** + * @brief Constructs a scatter_array from any number of spanable containers. + * + * @details Each argument is converted to @c std::span and stored in + * the internal array. The base @c scatter_span is then initialized to view + * that array. @p N must equal @c sizeof...(p_spans). + * + * @tparam Spans Pack of spanable container types (e.g. @c std::array, + * @c std::vector). + * @param p_spans Containers whose spans form the logical sequence. + */ + template + constexpr scatter_array(Spans&&... p_spans) + : detail::scatter_array_storage{ .m_internal_arr = { std::span( + p_spans)... } } + , scatter_span(this->m_internal_arr) + { + } +}; + +/** + * @brief Deduction guide for @c scatter_array. + * + * @details Deduces @c T from the value type of the first argument and @c N + * from the total argument count. All arguments must have the same element type. + */ +template + requires( + std::same_as< + typename decltype(std::span(std::declval()))::value_type, + typename decltype(std::span(std::declval()))::value_type> && + ...) +scatter_array(First& p_first, Spans&&... p_spans) -> scatter_array< + typename std::remove_reference_t::value_type, + sizeof...(p_spans) + 1>; + +} // namespace mem diff --git a/modules/scatter_span.cppm b/modules/scatter_span.cppm index 309fe1e..27e57bc 100644 --- a/modules/scatter_span.cppm +++ b/modules/scatter_span.cppm @@ -1,21 +1,2 @@ -module; - -#include -#include -#include - export module scatter_span; - -export namespace mem { - -// Placeholder for scatter_span data structure - -class scatter_span -{}; - -template -class scatter_array : scatter_span -{ - std::array, N> m_internal_arr; -}; -} // namespace mem +export import :core; diff --git a/tests/scatter_span.test.cpp b/tests/scatter_span.test.cpp index de6b1b1..83a5ff4 100644 --- a/tests/scatter_span.test.cpp +++ b/tests/scatter_span.test.cpp @@ -12,17 +12,144 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include #include +#include +#include +#include +#include import scatter_span; -namespace { -boost::ut::suite<"scatter_span"> scatter_span_test = [] { +namespace mem { + +} // namespace mem + +namespace mem { + +// Exclusively exists to do comparisons, there isn't really a need for a +// comparison between scatter spans. +// if discovered +template +bool scatter_span_eq(mem::scatter_span const& lhs, + mem::scatter_span const& rhs) +{ + auto len = lhs.length(); + if (len != rhs.length()) { + return false; + } + + if (len == 0) { + return true; + } + + std::vector lhs_vec; + std::vector rhs_vec; + + for (auto s : lhs) { + lhs_vec.append_range(s); + } + + for (auto s : rhs) { + rhs_vec.append_range(s); + } + + for (size_t i = 0; i < lhs_vec.size(); i++) { + if (lhs_vec[i] != rhs_vec[i]) { + return false; + } + } + + return true; +} + +template +void print_scatter_span(scatter_span const& p_ssp) +{ + std::print("["); + for (auto s : p_ssp) { + std::print("{}, ", s); + } + std::println("]"); +} + +template +void print_scatter_span_addrs(scatter_span const& p_ssp) +{ + size_t count = 0; + for (auto s : p_ssp) { + std::println("{}: {:#x}, ", count, reinterpret_cast(&s[0])); + count++; + } + std::println(""); +} + +} // namespace mem + +boost::ut::suite<"scatter_span"> basic_scatter_span_tests = [] { using namespace boost::ut; + using namespace mem; - "placeholder_test"_test = [] { expect(true); }; -}; + std::array first = { 1, 2, 3 }; + std::array second{ 4, 5 }; + std::array third = { 6, 7, 8, 9 }; + std::array expected = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + + "ctor_and_len"_test = [&] { + mem::scatter_array ssp(first, second, third); + expect(that % ssp.length() == 9); + + mem::scatter_array expected_ssp({ expected }); + expect(that % scatter_span_eq(ssp, expected_ssp)); + }; + + "api_lifetime"_test = [&] { + auto mock_api = [&first, &second, &third](scatter_span p_ssp) { + std::array expected = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + mem::scatter_array expected_ssp(expected); + expect(that % scatter_span_eq(expected_ssp, p_ssp)); + + auto ssp_it = p_ssp.begin(); + expect(that % first.data() == ssp_it->data()); + ssp_it++; + expect(that % second.data() == ssp_it->data()); + ssp_it++; + expect(that % third.data() == ssp_it->data()); + }; + + mock_api({ first, second, third }); + }; + + "subscatterspan"_test = [&] { + auto ssa = scatter_array(first, second, third); -} // namespace + auto subssp = ssa.sub_scatter_span({ .offset = 0, .count = 5 }); + + expect(that % + scatter_span_eq(subssp, { std::span(expected).subspan(0, 5) })); + + auto uneven_ssp = ssa.sub_scatter_span({ .offset = 0, .count = 6 }); + + print_scatter_span(uneven_ssp); + expect(that % + scatter_span_eq(uneven_ssp, { std::span(expected).subspan(0, 6) })); + + auto uneven_ssp_two = ssa.sub_scatter_span({ .offset = 0, .count = 2 }); + expect(that % scatter_span_eq(uneven_ssp_two, + { std::span(expected).subspan(0, 2) })); + + auto offset_even_ssp = ssa.sub_scatter_span({ .offset = 2 }); + expect(that % offset_even_ssp.length() == (ssa.length() - 2)); + expect(that % scatter_span_eq(offset_even_ssp, + { std::span(expected).subspan(2) })); + + auto uneven_offset_ssp = ssa.sub_scatter_span({ .offset = 3, .count = 5 }); + expect(that % scatter_span_eq(uneven_offset_ssp, + { std::span(expected).subspan(3, 5) })); + + auto offset_greater_than_len = ssa.sub_scatter_span({ .offset = 9 }); + expect(that % scatter_span_eq(offset_greater_than_len, {})); + }; +}; int main() {