What is the purpose of the `iterator-sentinel-pair` concept in [ranges.subrange]?

Viewed 110

What's the purpose of the exposition-only iterator-sentinel-pair concept as defined in [range.subrange]?

template<class T> 
concept iterator-sentinel-pair = // exposition only 
  !range<T> && pair-like<T> && 
  sentinel_for<tuple_element_t<1, T>, tuple_element_t<0, T>>;

The only time it's used, as far as I can see, is for CTAD, such as in

template<iterator-sentinel-pair P>
  subrange(P) -> subrange<tuple_element_t<0, P>, tuple_element_t<1, P>>;

However, there aren't any constructors of std::ranges::subrange that will work with this concept: the only real constructor possibly fitting this parameter list is this one:

template<not-same-as<subrange> R> 
  requires borrowed_range<R> &&
           convertible-to-non-slicing<iterator_t<R>, I> &&
           convertible_to<sentinel_t<R>, S>
constexpr subrange(R&& r) requires (!StoreSize || sized_range<R>);

which requires (through borrowed_range<R>) that we have range<R>... but iterator-sentinel-pair explicitly requires not range<R>!

It looks to me that the intention was to allow code along the lines of

std::multiset foo = // ...
auto result = foo.equal_range(key);  // an iterator-sentinel pair
for (auto value : std::ranges::subrange(result)) {
  // ...
}

but this clearly will not compile, as std::pair<It, It> doesn't fulfil the requirements for std::range. Maybe the constructor to allow for this use-case was overlooked?

Is there some other purpose I'm missing?

1 Answers

What's the purpose of the exposition-only iterator-sentinel-pair concept as defined in [range.subrange]?

No purpose anymore, they're removed as of LWG 3404 adopted at the previous plenary, and will shortly be removed from the draft. They're just vestigial oversight after the all the stuff that used them was removed in LWG 3281.


Originally, the purpose of the concept was precisely to allow this:

for (auto value : std::ranges::subrange(foo.equal_range(key))) {
    // ...
}

but the implicitness was removed based on the reasoning that:

Just because a pair is holding two iterators, it doesn't mean those two iterators denote a valid range. Implicitly converting such pair-like types to a subrange is dangerous and should be disallowed.

Though it seems useful to at least have an explicit conversion. Thankfully, it's straightforward to write ourselves:

template <typename P>
    requires (std::tuple_size_v<P> == 2)
          && std::ranges::sentinel_for<std::tuple_element_t<1, P>,
                                       std::tuple_element_t<0, P>>
auto make_subrange_from_pair(P pair) {
    // either this
    auto [b, e] = pair;
    return subrange(b, e);
    // or that
    return subrange(std::get<0>(pair), std::get<1>(pair));
}
Related