as_rvalue_view::end should improve non-common caseSection: 25.7.7.2 [range.as.rvalue.view] Status: New Submitter: Hewill Kang Opened: 2022-11-13 Last modified: 2022-11-30
Priority: 3
View all issues with New status.
Discussion:
Currently, when the underlying range is not a common range, as_rvalue_view::begin and end return
move_iterator and move_sentinel respectively, this seems reasonable since move_sentinel
is a sentinel adaptor introduced by C++20 specifically for comparison with move_iterator.
#include <list>
#include <ranges>
int main() {
std::list<std::tuple<int&>> l;
auto k = std::move(l) | std::views::keys;
auto s = std::ranges::subrange(std::cbegin(k), std::end(k));
(void) std::ranges::next(s.begin(), s.end()); // constant time
auto r = s | std::views::as_rvalue;
(void) std::ranges::next(r.begin(), r.end()); // linear time
}
The above subrange is constructed by the elements_view::iterator<true> and
elements_view::iterator<false> pair, and since the former can be assigned by the latter,
when we use ranges::next to increment the begin of s to its end, the
assignable_from branch will be executed, so we get a constant-time complexity.
views::as_rvalue to s, the as_rvalue_view::end will go
into the non-common branch and return move_sentinel. And because move_iterator cannot be
assigned by move_sentinel, ranges::next will successively increment the begin of s
until its end, we get the linear-time complexity this time.
I think it is more appropriate to return move_iterator for the above case, as this preserves the
assignability, but also preserves the iterator operability that the original sentinel type has.
Another benefit of doing this is that when the sentinel type of underlying range can be subtracted from its
iterator type but does not model sized_sentinel_for, returning different move_iterator makes
them still subtractable, because its operator- only constrain the x.base() - y.base() being
well-formed.
This also solves the issue of as_rvalue_view being a valid type but does not model a range in
some cases, for example:
#include <ranges>
int main() {
std::move_iterator<int*> i;
std::move_iterator<const int*> ci;
std::ranges::subrange s(i, ci);
std::ranges::as_rvalue_view r(s); // not failed
static_assert(std::ranges::range<decltype(r)>); // failed
}
This is because currently, as_rvalue_view does not explicitly specify the template parameters of the returned
move_iterator and move_sentinel, so based on CTAD, its begin will return
move_iterator(move_iterator(...)) which is still move_iterator<int*>, and its end will
return move_sentinel<move_iterator<const int*>>. Those two types are not comparable, so r
does not constitute a valid range.
move_iterator when the sentinel type of the underlying range models
input_iterator.
[2022-11-30; Reflector poll]
Set priority to 3 after reflector poll.
"NAD, these examples seem entirely contrived. If not NAD, don't need the
common_range check if we are checking that the sentinel models
input_iterator."
Proposed resolution:
This wording is relative to N4917.
Modify 25.7.7.2 [range.as.rvalue.view] as indicated:
namespace std::ranges {
template<view V>
requires input_range<V>
class as_rvalue_view : public view_interface<as_rvalue_view<V>> {
[…]
constexpr auto begin() requires (!simple-view<V>)
{ return move_iterator(ranges::begin(base_)); }
constexpr auto begin() const requires range<const V>
{ return move_iterator(ranges::begin(base_)); }
constexpr auto end() requires (!simple-view<V>) {
if constexpr (common_range<V> || input_iterator<sentinel_t<V>>) {
return move_iterator(ranges::end(base_));
} else {
return move_sentinel(ranges::end(base_));
}
}
constexpr auto end() const requires range<const V> {
if constexpr (common_range<const V> || input_iterator<sentinel_t<const V>>) {
return move_iterator(ranges::end(base_));
} else {
return move_sentinel(ranges::end(base_));
}
}
[…]
};
[…]
}