C++ Logo

std-proposals

Advanced search

[std-proposals] `std::counted_sentinel`?

From: Hewill Kang <hewillk_at_[hidden]>
Date: Mon, 18 Dec 2023 12:35:32 +0800
Hello C++ experts,

I wonder why the standard has std::move_sentinel but not
std::counted_sentinel, because in my opinion the latter is super valuable
in certain situations.

Suppose we have a non-sized range:

 auto l = std::list{1, 2, 0, 3, 42, 42, 5, 6, 7, 9, 10, 42, 0, 11};
 auto r = l | std::views::filter([](int x) { return x != 42; });

And we want to apply views::split or views::chunk_by to it to split it into
subranges (https://godbolt.org/z/sKh3zM1Wf):

auto split = r | std::views::split(0);
for (auto subrange : split)
  std::println("{} (size: {})", subrange, std::ranges::distance(subrange));
auto chunk = r | std::views::chunk_by(std::less{});
for (auto subrange : chunk)
  std::println("{} (size: {})", subrange, std::ranges::distance(subrange));

Unfortunately, these subranges are not sized_range even if we have iterated
through them from beginning to end, since they just contain two iterators
of the original range which cannot be subtracted.

In order to get their sizes, I have to recalculate their sizes using
ranges::distance, which makes it necessary to iterate over them again.

However, if we introduce a std::counted_sentinel corresponding to
std::counted_iterator for comparison with the former's underlying iterator,
for example:

namespace std {

template<semiregular S>
struct counted_sentinel {
  counted_sentinel() = default;
  constexpr explicit counted_sentinel(S end) : end_(std::move(end)) { }
  constexpr S base() const { return end_; }
  template<class I> requires sentinel_for<S, I>
  friend constexpr bool operator==(
    const counted_iterator<I>& x, const counted_sentinel& y)
      { return x.base() == y.base(); }
private:
  S end_;
};

}

This allows us to wrap the origin range from the previous example to get
subranges that model sized_range (https://godbolt.org/z/arhdqMKxo):

auto l = std::list{1, 2, 0, 3, 42, 42, 5, 6, 7, 9, 10, 42, 0, 11};
auto r = l | std::views::filter([](int x) { return x != 42; });
auto s = std::ranges::subrange(std::counted_iterator(r.begin(), 0),
                               std::counted_sentinel(r.end()));
auto split = s | std::views::split(0);
for (auto subrange : split)
  std::println("{} (size: {})", subrange, subrange.size());
auto chunk = s | std::views::chunk_by(std::less{});
for (auto subrange : chunk)
  std::println("{} (size: {})", subrange, subrange.size());

I think this is a big enhancement to avoid redundant calculations, and I
think this can also be applied to algorithms that return subranges such as
ranges::find_last etc. so that they always return a subrange that models a
sized_range.

What do you guys think? Is this worth adding to the standard? I believe
there may be more useful usage models that I haven't thought of yet.

Hewill

Received on 2023-12-18 04:35:45