Skip to content

feat(stride_view): add a strided random-access range adaptor#860

Draft
IvanaGyro wants to merge 2 commits into
claude/835-pairwise-sumfrom
claude/stride-view
Draft

feat(stride_view): add a strided random-access range adaptor#860
IvanaGyro wants to merge 2 commits into
claude/835-pairwise-sumfrom
claude/stride-view

Conversation

@IvanaGyro
Copy link
Copy Markdown
Collaborator

Stacked on #849. Base is claude/835-pairwise-sum; this PR's diff is only the stride_view header + its test. Review/merge #849 first, then this PR retargets to master.

Summary

Adds cytnx::linalg_internal::stride_view<V>, a strided random-access range adaptor that wraps any sized random-access range V and exposes every step-th element as a fresh random-access range. The r | stride(k) pipe form lets range algorithms consume strided slices directly.

The iterator stores the underlying base iterator (not a pointer back to the view), so iterators stay valid as long as the underlying data lives — independent of the view's lifetime.

Why now

The next Trace refactor needs to reduce a tensor diagonal in place without first cloning the data into a contiguous buffer. Landing stride_view as its own change makes the iterator semantics reviewable in isolation and lets the diagonal-sum step in the trace PR focus on the trace algorithm itself.

Tests

tests/linalg_test/stride_view_test.cpp covers:

  • Random-access iterator concept model via static_assert(std::random_access_iterator<...>) / std::ranges::random_access_range<...> / std::ranges::sized_range<...>.
  • Strided element selection (dividing and non-dividing sizes — the n % step != 0 branch of size()).
  • step == 0 precondition (EXPECT_THROW(stride_view(..., 0), std::invalid_argument)).
  • Pipe adaptor (r | stride(k)) fed into PairwiseSum to exercise the integration point.
  • Iterator-outlives-view regression case.
  • Random-access operations (+ n, [n], end - begin, <).
  • Complex strided sum.

The test reaches into the internal header through a new ${CMAKE_SOURCE_DIR}/src PRIVATE include on the test binary, so internal utilities can be unit-tested directly.

Test plan

  • openblas-cpu: full suite 955/955 passed.
  • mkl-cpu: full suite 955/955 passed.
  • pre-commit (clang-format-14 + EOF + whitespace) clean.

Draft — opened for review.


Generated by Claude Code

`stride_view<V>` wraps any sized random-access range `V` and presents every
`step`-th element as a fresh random-access range, with a `r | stride(k)` pipe
form so range algorithms can consume strided slices directly. The iterator
stores the underlying base iterator (not a pointer back to the view), which
keeps iterators valid as long as the underlying data lives, independent of the
view's lifetime.

This is the building block the next Trace refactor needs to reduce a tensor
diagonal in place without first cloning the data into a contiguous buffer.
Landing it as its own change makes the iterator semantics reviewable in
isolation and lets the diagonal-sum step focus on the trace algorithm itself.

`tests/linalg_test/stride_view_test.cpp` covers the random-access iterator
concept model (via `static_assert`), strided element selection, the
non-dividing-size branch of `size()`, the `step == 0` precondition, the pipe
adaptor (fed into `PairwiseSum` to exercise the integration point), the
iterator-outlives-view case, the random-access operations, and a complex
strided sum. The test reaches into the internal header through a new
`${CMAKE_SOURCE_DIR}/src` PRIVATE include on the test binary so internal
utilities can be unit-tested directly.

Co-authored-by: Claude <noreply@anthropic.com>
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces stride_view, a view utility over every step-th element of a random-access, sized range, along with its corresponding unit tests. The review feedback highlights two potential issues in the iterator implementation: first, operator[] performs unsigned addition which can cause unexpected behavior when indexing with negative values; second, operator-= is implemented via negation (-n), which can trigger undefined behavior due to signed overflow if n is the minimum value of difference_type. It is recommended to perform these calculations in signed space and implement subtraction directly.

Comment thread src/backend/linalg_internal_cpu/stride_view.hpp
Comment thread src/backend/linalg_internal_cpu/stride_view.hpp Outdated
Comment thread src/backend/linalg_internal_cpu/stride_view.hpp Outdated
}

iterator& operator+=(difference_type n) {
index_ = static_cast<std::size_t>(static_cast<difference_type>(index_) + n);
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if static_cast<difference_type>(index_) + n < 0? How will common lib like stl handle this case? Raise an error?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the 1e86067 fix, static_cast<difference_type>(index_) + n < 0 is computed in signed space and produces a negative index_ (after the cast back), which is exactly how the STL's __normal_iterator / pointer iterators behave: stepping before begin() is undefined behavior, not a checked error — no STL container iterator throws here. stride_view::iterator matches that contract (it models random_access_iterator, whose operator[]/+= have a narrow contract and no bounds check). Reaching a negative index is only valid as an intermediate when you step back to a valid position before dereferencing; RandomAccessOps exercises that with mid[-2] and *(end() - 1). Adding a throw would make it non-conforming (and slower in the hot diagonal-sum loop).


Generated by Claude Code

Comment thread src/backend/linalg_internal_cpu/stride_view.hpp Outdated
Comment thread src/backend/linalg_internal_cpu/stride_view.hpp Outdated

// Range-adaptor closure so a strided view can be written as
// `range | stride(step)`, e.g. `std::span<const T>(data, extent) | stride(k)`.
struct stride_closure {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we put this in views like std::ranges::views::stride() <=> std::ranges::stride_view

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept it as cytnx::linalg_internal::stride / stride_view rather than a views:: namespace, for two reasons:

  1. C++20 has no std::ranges::views::stride — that's a C++23 addition (std::ranges::stride_view). Mirroring the views:: spelling under our own namespace would invite confusion with the standard one once we move to C++23, where we'd want to just delete this and use std::views::stride.
  2. This is an internal backend utility (linalg_internal), not a public range adaptor we're committing to as API. The flat stride() closure factory is enough for the one consumer (Trace).

If/when the project moves to C++23 this whole header can be replaced by std::views::stride; I left the pipe form r | stride(k) deliberately matching that future signature so the call sites won't change. Happy to add a // TODO(C++23): replace with std::views::stride if you'd like that recorded in the source.


Generated by Claude Code

Comment thread tests/linalg_test/stride_view_test.cpp
stride_view sv(std::span<const double>(v), 2); // 0,2,4,...,18
auto it = sv.begin();
EXPECT_DOUBLE_EQ(*(it + 3), 6.0);
EXPECT_DOUBLE_EQ(it[4], 8.0);
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test random access with negative indices/diff.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1e86067. RandomAccessOps now exercises negative offsets: auto mid = sv.begin() + 5; EXPECT_DOUBLE_EQ(mid[-2], 6.0); (negative subscript) and EXPECT_DOUBLE_EQ(*(sv.end() - 1), 18.0) (backward step from end()). These are exactly the paths the signed-space operator[] / operator-= fixes make well-defined.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

@IvanaGyro IvanaGyro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Review written by Claude on behalf of @IvanaGyro.)

Clean adaptor and it resolves both concerns from the earlier round: the test file is now wired into tests/CMakeLists.txt (with src/ exposed for the internal header), and the iterator stores the base iterator rather than a back-pointer to the view, with IteratorsOutliveTheView locking that in. The static_asserts on the iterator/range concepts plus the pipe-adaptor + PairwiseSum integration tests are exactly the right shape. One small edge-case test gap noted inline.

Comment thread src/backend/linalg_internal_cpu/stride_view.hpp
…tests

stride_view::iterator::operator-= computed `*this += -n`, which negates the
signed difference_type argument. For n == std::numeric_limits<difference_type>::min()
the negation overflows -- undefined behavior. Subtract the offset directly so
no value of n is negated.

Reorder declarations so each class lists its public interface before its data
members, matching the Google C++ style guide's declaration order.

Document the view with doxygen-style block comments: class purpose with a
short usage example, constructor preconditions, size() contract on empty and
sub-stride ranges, and the stride() / stride_closure pipe adaptor.

Expand the iterator and view tests to cover boundaries reviewers flagged:

* SizeRoundsUpWhenLengthNotMultipleOfStride: a 10-element range with stride 3
  yields 4 elements (indices 0, 3, 6, 9), pinning the (n + step - 1) / step
  rounding in size() and end() that the existing exact-multiple case does
  not reach.
* EmptyBaseYieldsEmptyView: size()==0, begin()==end(), and PairwiseSum over
  the empty piped view returns the identity.
* StrideLargerThanBaseSelectsFirstElement: a 3-element range with stride 10
  yields a single-element view containing the first element.
* RandomAccessOps: a negative subscript (mid[-2]) and a backward step from
  end() (*(end() - 1)). Because step_ is the signed difference_type, n * step_
  stays signed and negative offsets step backwards rather than wrapping.

Co-authored-by: Claude <noreply@anthropic.com>
@IvanaGyro IvanaGyro force-pushed the claude/stride-view branch from d85fd93 to 1e86067 Compare May 31, 2026 12:18
@pcchen pcchen added this to the v1.1.0 milestone Jun 1, 2026
Copy link
Copy Markdown
Collaborator Author

Pushed 1e86067 addressing this round:

  • operator-= UB — now subtracts directly instead of *this += -n, so difference_type::min() is never negated.
  • operator[] signed arithmetic — index math done in difference_type space; negative offsets no longer wrap through std::size_t.
  • Declaration order — both stride_view and iterator list the public interface before private data members.
  • Doxygen — class, iterator, ctor, size(), and stride()/stride_closure documented with @brief/@param/@throws/@par Example.
  • Tests — added EmptyBaseYieldsEmptyView, StrideLargerThanBaseSelectsFirstElement, SizeRoundsUpWhenLengthNotMultipleOfStride, and negative random-access ops (mid[-2], *(end()-1)) in RandomAccessOps. 9/9 StrideView tests pass; full openblas-cpu suite 965/965.

Left open for your call: the views::-namespace placement (C++23 would let us drop this for std::views::stride) and whether to make step signed with an explicit negative-rejection. Replied inline on both.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants