stride_view
Document #: | P1899R2 |
Date: | 2021-12-20 |
Project: | Programming Language C++ |
Audience: |
SG9 LEWG |
Reply-to: |
Christopher Di Bella <[email protected]> Tim Song <[email protected]> |
The ability to use algorithms over an evenly-spaced subset of a range has been missed in the STL for a quarter of a century. Given that there’s no way to compose a strided range adaptor in C++20, this should be adopted for C++23.
chunk
from [P2442R1].iterator_concept
, and corrects iterator_category
so it can’t be contiguous.compute-distace
so they pass in size of underlying range instead of themselves.Initial revision.
The ability to use algorithms over an evenly-spaced subset of a range has been missed in the STL for a quarter of a century. This is, in part, due to the complexity required to use an iterator that can safely describe such a range. It also means that the following examples cannot be transformed from raw loops into algorithms, due to a lacking iterator.
namespace stdr = std::ranges;
namespace stdv = std::views;
for (auto i = 0; i < ssize(v); i += 2) {
v[i] = 42; // fill
}
for (auto i = 0; i < ssize(v); i += 3) {
v[i] = f(); // transform
}
for (auto i = 0; i < ssize(v); i += 3) {
for (auto j = i; j < ssize(v); i += 3) {
if (v[j] < v[i]) {
stdr::swap(v[i], v[j]); // selection sort, but hopefully the idea is conveyed
}
}
}
Boost.Range 2.0 introduced a range adaptor called strided
, and range-v3’s equivalent is stride_view
, both of which make striding far easier than when using iterators:
stdr::fill(v | stdv::stride(2), 42);
auto strided_v = v | stdv::stride(3);
stdr::transform(strided_v, stdr::begin(strided_v) f);
stdr::stable_sort(strided_v); // order restored!
Given that there’s no way to compose a strided range adaptor in C++20, this should be one of the earliest range adaptors put into C++23.
stride_view
Although it isn’t possible to compose stride_view
in C++20, someone inexperienced with the ranges design space might mistake filter_view
as a suitable way to “compose” stride_view
:
auto bad_stride = [](auto const step) {
return views::filter([n = 0, step](auto&&) mutable {
return n++ % step == 0;
});
};
This implementation is broken for two reasons:
filter_view
expects a predicate
as its input, but the lambda we have provided does not model predicate
(a call to invoke
on a predicate
mustn’t modify the function object, yet we clearly are).bidirectional_iterator
, it does not model the concept, thus rendering any program containing it ill-formed, with no diagnostic being required.For these reasons, the author regrets not proposing this in the C++20 design space.
Both Boost.Range 2.0 and range-v3 are popular ranges libraries that support a striding range adaptor. The proposed wording has mostly been implemented in cmcstl2 and in a CppCon main session.
Boost.Range 2.0’s strided
has a precondition that 0 <= n
, but this isn’t strong enough: we need n
to be positive.
The stride needs to be positive since a negative stride doesn’t really make sense, and a semantic requirement of std::weakly_incrementable
(23.3.4.4 [iterator.concept.winc]) is that incrementing actually moves the iterator to the next element: this means a zero-stride isn’t allowed either.
LEWG unanimously agreed that this was the correct decision in Prague.
A simple implementation of stride_view
would be similar to what’s in Boost.Range 2.0: a single-pass range adaptor. With some effort, we can go all the way to a random-access range adaptor, which is what this section mainly covers.
A naive random-access range adaptor would be implemented by simply moving the iterator forward or backward by n
positions (where n
is the stride length). While this produce a correct iterator when moving forward, its operator--
will be incorrect whenever n
doesn’t evenly divide the underlying range’s length. For example:
auto x = std::vector{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
// prints 0 3 6 9
stdr::copy(stdv::stride(x, 3), std::ostream_iterator<int>(std::cout, " "));
// prints 9 6 3 0
stdr::copy(stdv::stride(x, 3) | stdv::reverse, std::ostream_iterator<int>(std::cout, " "));
auto y = std::vector{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// prints 0 3 6 9
stdr::copy(stdv::stride(y, 3), std::ostream_iterator<int>(std::cout, " "));
// prints 8 5 2: not the same range in reverse!?
stdr::copy(stdv::stride(y, 3) | stdv::reverse, std::ostream_iterator<int>(std::cout, " "));
The problem here is that going from the one-before-past-the-end iterator to the past-the-end iterator may involve iterating fewer than stride
steps, so to correctly iterate backwards, we need to know that distance.
This is the same problem as chunk
([P2442R1]) and can be solved in the same way. After all, stride(n)
is basically a more efficient version of chunk(n) | transform(ranges::front)
(if we actually had a ranges::front
).
stride_view
is borrowed if the underlying view is. It is a common range if the underlying range is common and either sized or non-bidirectional.
<ranges>
Add the following to 24.2 [ranges.syn], header <ranges>
synopsis:
// [...]
namespace std::ranges {
// [...]
// [range.stride], stride view
template<input_range V>
requires view<V>
class stride_view;
template<class V>
inline constexpr bool enable_borrowed_range<stride_view<V>> =
enable_borrowed_range<V>;
namespace views { inline constexpr unspecified stride = unspecified; }
// [...]
}
stride
Add the following subclause to 24.7 [range.adaptors].
[ Editor's note: This wording assumes the exposition-only div-ceil
function template introduced in [P2442R1]. ]
1 stride_view
presents a view of an underlying sequence, advancing over n
elements at a time, as opposed to the usual single-step succession.
2 The name views::stride
denotes a range adaptor object 24.7.2 [range.adaptor.object]. Given subexpressions E
and N
, the expression views::stride(E, N)
is expression-equivalent to stride_view(E, N)
.
3 [Example:
auto input = views::iota(0, 12) | views::stride(3);
ranges::copy(input, ostream_iterator<int>(cout, " ")); // prints 0 3 6 9
ranges::copy(input | views::reverse, ostream_iterator<int>(cout, " ")); // prints 9 6 3 0
— end example]
stride_view
[range.stride.view]namespace std::ranges {
template<input_range V>
requires view<V>
class stride_view : public view_interface<stride_view<V>> {
V base_ = V(); // exposition only
range_difference_t<V> stride_ = 1; // exposition only
template<bool> class iterator; // exposition only
public:
stride_view() requires default_initializable<V> = default;
constexpr explicit stride_view(V base, range_difference_t<V> stride);
constexpr V base() const& requires copy_constructible<V> { return base_; }
constexpr V base() && { return std::move(base_); }
constexpr range_difference_t<V> stride() const noexcept;
constexpr auto begin() requires (!simple-view<V>) {
return iterator<false>(this, ranges::begin(base_));
}
constexpr auto begin() const requires range<const V> {
return iterator<true>(this, ranges::begin(base_));
}
constexpr auto end() requires (!simple-view<V>) {
if constexpr (common_range<V> && sized_range<V> && forward_range<V>) {
auto missing = (stride_ - ranges::distance(base_) % stride_) % stride_;
return iterator<false>(this, ranges::end(base_), missing);
}
else if constexpr (common_range<V> && !bidirectional_range<V>) {
return iterator<false>(this, ranges::end(base_));
}
else {
return default_sentinel;
}
}
constexpr auto end() const requires range<const V> {
if constexpr (common_range<const V> && sized_range<const V> && forward_range<const V>) {
auto missing = (stride_ - ranges::distance(base_) % stride_) % stride_;
return iterator<true>(this, ranges::end(base_), missing);
}
else if constexpr (common_range<const V> && !bidirectional_range<const V>) {
return iterator<true>(this, ranges::end(base_));
}
else {
return default_sentinel;
}
}
constexpr auto size() requires sized_range<V>;
constexpr auto size() const requires sized_range<const V>;
};
template<class R>
stride_view(R&&, range_difference_t<R>) -> stride_view<views::all_t<R>>;
}
[ Drafting note: end()
cannot compute missing
for input-only ranges because ranges::size
(and ranges::distance
by extension) is not required to be valid after ranges::begin
is called, but end()
must remain callable. ]
1 Preconditions:
stride > 0
istrue
.2 Effects: Initializes
base_
withstd::move(base)
andstride_
withstride
.
3 Returns:
stride_
.
constexpr auto size() requires sized_range<V>;
constexpr auto size() const requires sized_range<const V>;
4 Effects: Equivalent to:
stride_view::iterator
[range.stride.iterator]namespace std::ranges {
template<input_range V>
requires view<V>
template<bool Const>
class stride_view<V>::iterator {
using Parent = maybe-const<Const, chunk_view>; // exposition only
using Base = maybe-const<Const, V>; // exposition only
iterator_t<Base> current_ = iterator_t<Base>(); // exposition only
sentinel_t<Base> end_ = sentinel_t<Base>(); // exposition only
range_difference_t<Base> stride_ = 0; // exposition only
range_difference_t<Base> missing_ = 0; // exposition only
constexpr iterator(Parent* parent, iterator_t<Base> current, // exposition only
range_difference_t<Base> missing = 0);
public:
using difference_type = range_difference_t<Base>;
using value_type = range_value_t<Base>;
using iterator_concept = see below;
using iterator_category = see below; // not always present
iterator() requires default_initializable<iterator_t<Base>>= default;
constexpr iterator(iterator<!Const> other)
requires Const && convertible_to<iterator_t<V>, iterator_t<Base>>
&& convertible_to<sentinel_t<V>, sentinel_t<Base>>;
constexpr iterator_t<Base> base() &&;
constexpr const iterator_t<Base>& base() const & noexcept;
constexpr decltype(auto) operator*() const { return *current_; }
constexpr iterator& operator++();
constexpr void operator++(int);
constexpr iterator operator++(int) requires forward_range<Base>;
constexpr iterator& operator--() requires bidirectional_range<Base>;
constexpr iterator operator--(int) requires bidirectional_range<Base>;
constexpr iterator& operator+=(difference_type n) requires random_access_range<Base>;
constexpr iterator& operator-=(difference_type n) requires random_access_range<Base>;
constexpr decltype(auto) operator[](difference_type n) const
requires random_access_range<Base>
{ return *(*this + n); }
friend constexpr bool operator==(const iterator& x, default_sentinel);
friend constexpr bool operator==(const iterator& x, const iterator& y)
requires equality_comparable<iterator_t<Base>>;
friend constexpr bool operator<(const iterator& x, const iterator& y)
requires random_access_range<Base>;
friend constexpr bool operator>(const iterator& x, const iterator& y)
requires random_access_range<Base>;
friend constexpr bool operator<=(const iterator& x, const iterator& y)
requires random_access_range<Base>;
friend constexpr bool operator>=(const iterator& x, const iterator& y)
requires random_access_range<Base>;
friend constexpr auto operator<=>(const iterator& x, const iterator& y)
requires random_access_range<Base> && three_way_comparable<iterator_t<Base>>;
friend constexpr iterator& operator+(const iterator& x, difference_type n)
requires random_access_range<Base>;
friend constexpr iterator& operator+(difference_type n, const iterator& x)
requires random_access_range<Base>;
friend constexpr iterator& operator-(const iterator& x, difference_type n)
requires random_access_range<Base>;
friend constexpr difference_type operator-(const iterator& x, const iterator& y)
requires sized_sentinel_for<iterator_t<Base>, iterator_t<Base>>;
friend constexpr difference_type operator-(default_sentinel_t y, const iterator& x)
requires sized_sentinel_for<sentinel_t<Base>, iterator_t<Base>>;
friend constexpr difference_type operator-(const iterator& x, default_sentinel_t y)
requires sized_sentinel_for<sentinel_t<Base>, iterator_t<Base>>;
friend constexpr range_rvalue_reference_t<Base> iter_move(const iterator& i)
noexcept(noexcept(ranges::iter_move(i.current_)));
friend constexpr void iter_swap(const iterator& x, const iterator& y)
noexcept(noexcept(ranges::iter_swap(x.current_, y.current_)))
requires indirectly_swappable<iterator_t<Base>>;
};
}
1 iterator::iterator_concept
is defined as follows:
(1.1) If Base
models random_access_range
, then iterator_concept
denotes random_access_iterator_tag
.
(1.2) Otherwise, if Base
models bidirectional_range
, then iterator_concept
denotes bidirectional_iterator_tag
.
(1.3) Otherwise, if Base
models forward_range
, then iterator_concept
denotes forward_iterator_tag
.
(1.4) Otherwise, iterator_concept
denotes input_iterator_tag
.
2 The member typedef-name iterator_category
is defined if and only if Base
models forward_range
. In that case, iterator::iterator_category
is defined as follows:
(2.1) Let C
denote the type iterator_traits<iterator_t<Base>>::iterator_category
.
(2.2) If C
models derived_from<random_access_iterator_tag>
, then iterator_category
denotes random_access_iterator_tag
.
(2.3) Otherwise, iterator_category
denotes C
.
3 Effects: Initializes
current_
withstd::move(current)
,end_
withranges::end(parent->base_)
,stride_
withparent->stride_
, andmissing_
withmissing
.
constexpr iterator(iterator<!Const> i)
requires Const && convertible_to<iterator_t<V>, iterator_t<Base>>
&& convertible_to<sentinel_t<V>, sentinel_t<Base>>;
4 Effects: Initializes
current_
withstd::move(i.current_)
,end_
withstd::move(i.end_)
,stride_
withi.stride_
, andmissing_
withi.missing_
.
5 Returns:
std::move(current_)
.
6 Returns:
current_
.
7 Preconditions:
current_ != end_
istrue
.8 Effects: Equivalent to:
9 Effects: Equivalent to:
++*this;
10 Effects: Equivalent to:
11 Effects: Equivalent to:
12 Preconditions: If
n
is positive,ranges::distance(current_, end_) > stride_ * (n - 1)
istrue
. [ Note 1: Ifn
is negative, the Effects: paragraph implies a precondition. — end note ]13 Effects: Equivalent to:
14 Effects: Equivalent to:
return *this += -x;
15 Returns:
x.current_ == x.end_;
friend constexpr bool operator==(const iterator& x, const iterator& y)
requires equality_comparable<iterator_t<Base>>;
16 Returns:
x.current_ == y.current_
.
friend constexpr bool operator<(const iterator& x, const iterator& y)
requires random_access_range<Base>;
17 Returns:
x.current_ < y.current_
.
friend constexpr bool operator>(const iterator& x, const iterator& y)
requires random_access_range<Base>;
18 Effects: Equivalent to:
return y < x;
friend constexpr bool operator<=(const iterator& x, const iterator& y)
requires random_access_range<Base>;
19 Effects: Equivalent to:
return !(y < x);
friend constexpr bool operator>=(const iterator& x, const iterator& y)
requires random_access_range<Base>;
20 Effects: Equivalent to:
return !(x < y);
friend constexpr auto operator<=>(const iterator& x, const iterator& y)
requires random_access_range<Base> &&
three_way_comparable<iterator_t<Base>>;
21 Returns:
x.current_ <=> y.current_
.
friend constexpr iterator operator+(const iterator& i, difference_type n)
requires random_access_range<Base>;
friend constexpr iterator operator+(difference_type n, const iterator& i)
requires random_access_range<Base>;
22 Effects: Equivalent to:
friend constexpr iterator operator-(const iterator& i, difference_type n)
requires random_access_range<Base>;
23 Effects: Equivalent to:
friend constexpr difference_type operator-(const iterator& x, const iterator& y)
requires sized_sentinel_for<iterator_t<Base>, iterator_t<Base>>;
24 Returns: Let
N
bex.current_ - y.current_
.
- (24.1) If
Base
modelsforward_range
,(N + x.missing_ - y.missing_) / x.stride_
.- (24.2) Otherwise, if
N
is negative,-div-ceil(-N, x.stride_)
.- (24.3) Otherwise,
div-ceil(N, x.stride_)
.[ Drafting note: When
Base
is input-only, the value ofmissing_
is unreliable. ]
friend constexpr difference_type operator-(default_sentinel_t y, const iterator& x)
requires sized_sentinel_for<sentinel_t<Base>, iterator_t<Base>>;
25 Returns:
div-ceil(x.end_ - x.current_, x.stride_)
.
friend constexpr difference_type operator-(const iterator& x, default_sentinel_t y)
requires sized_sentinel_for<sentinel_t<Base>, iterator_t<Base>>;
26 Effects: Equivalent to:
return -(y - x);
friend constexpr range_rvalue_reference_t<Base> iter_move(const iterator& i)
noexcept(noexcept(ranges::iter_move(i.current_)));
27 Effects: Equivalent to:
return ranges::iter_move(i.current_);
friend constexpr void iter_swap(const iterator& x, const iterator& y)
noexcept(noexcept(ranges::iter_swap(x.current_, y.current_)))
requires indirectly_swappable<iterator_t<Base>>;
28 Effects: Equivalent to:
ranges::iter_swap(x.current_, y.current_);
Add the following macro definition to 17.3.2 [version.syn], header <version>
synopsis, with the value selected by the editor to reflect the date of adoption of this paper:
The author would like to thank Tristan Brindle for providing editorial commentary on P1899, and also those who reviewed material for, or attended the aforementioned CppCon session or post-conference class, for their input on the design of the proposed stride_view
.
[P2442R1] Tim Song. 2021. Windowing range adaptors: views::chunk
and views::slide
.
https://wg21.link/P2442R1