4

There are four pair-like types in the standard, namely std::array, std::pair, std::tuple, and ranges::subrange, where the overload of std::get for ranges::subrange is defined in [range.subrange#access-10]:

template<size_t N, class I, class S, subrange_kind K>
  requires (N < 2)
  constexpr auto get(const subrange<I, S, K>& r);
template<size_t N, class I, class S, subrange_kind K>
  requires (N < 2)
  constexpr auto get(subrange<I, S, K>&& r);

Effects: Equivalent to:

if constexpr (N == 0)
  return r.begin(); 
else
  return r.end();

And ranges::subrange::begin() has two overloads:

constexpr I begin() const requires copyable<I>;
[[nodiscard]] constexpr I begin() requires (!copyable<I>);

I noticed that this std::get only has two overloads, and there is no corresponding overload for non-const lvalue reference, which makes it impossible for us to apply std::get to lvalue input_range with non-copyable iterator (godbolt):

#include <ranges>
#include <sstream>

int main() {
  auto ints = std::istringstream{"42"};
  auto is = std::ranges::istream_view<int>(ints);
  std::ranges::input_range auto r = std::ranges::subrange(is);
  auto b  = r.begin();      // OK
  auto b2 = std::get<0>(r); // Error, passing 'const subrange' as 'this' argument discards qualifiers
}

So why does std::get only have two function overloads for ranges::subrange? Does it miss the overloads for ranges::subrange& and const ranges::subrange&&? Is this a standard defect or is it intentional?

康桓瑋
  • 33,481
  • 5
  • 40
  • 90

1 Answers1

1

As a general rule, a fully-formed subrange in well-defined code represents a valid range, and if it stores a size, then the size is equal to the size of the range. This is reflected in the precondition of every non-default constructor (the default constructed state can still be partially-formed). This got slightly muddled by the introduction of move-only iterators (since the non-const begin needs to move the iterator out) but remains the design intent.

In other words, subrange is quite unlike pair, tuple, or array. Those three are aggregates of values with no semantics, and their get overloads reflect that by transparently propagating cv-qualification and value category. subrange, on the other hand, does have semantics - it's not merely a pair of iterator and sentinel. Its get is only for reading, never for writing. That's why get on a subrange returns by value.

It doesn't make much sense to provide a non-const lvalue get overload; returning by mutable reference is out of the question; returning by reference to const is unlike everything else (and also surprising). Returning by value means that in the only case it makes a difference, get on an lvalue subrange is a destructive operation, which would be quite unexpected.

T.C.
  • 133,968
  • 17
  • 288
  • 421
  • Another question is, should we make additional constraints for the first overload to eliminate the "discarded const qualifier" error that occurs inside the function body? I plan to submit an LWG for it, but I don't know if it is worth it. – 康桓瑋 Sep 08 '21 at 04:00
  • 1
    I see no reason to ban `get<1>` any more than `.end()`. It's entirely harmless and could be useful. I can see some value in checking that `begin` can be called in the constraint though. – T.C. Sep 08 '21 at 05:07