Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is the const overload of begin/end of the range adapters underconstrained?

In C++20, some ranges have both const and non-const begin()/end(), while others only have non-const begin()/end().

In order to enable the range adapters that wraps the former to be able to use begin()/end() when it is const qualified, some range adapters such as elements_view, reverse_view and common_view all provide constrained const-qualified begin()/end() functions, for example:

 template<view V>
   requires (!common_­range<V> && copyable<iterator_t<V>>)
 class common_view : public view_interface<common_view<V>> {
  public:
   constexpr auto begin();
   constexpr auto end();

   constexpr auto begin() const requires range<const V>;
   constexpr auto end()   const requires range<const V>;
};

template<input_­range V, size_t N>
  requires view<V> && has-tuple-element<range_value_t<V>, N>
class elements_view : public view_interface<elements_view<V, N>> {
 public:
  constexpr auto begin();
  constexpr auto end();

  constexpr auto begin() const requires range<const V>;
  constexpr auto end()   const requires range<const V>;
};

The begin() const/end() const will only be instantiated when const V satisfies the range, which seems reasonable.

But only one simple constraint seems to be insufficient. Take common_view for example, it requires that V is not a common_range. But when V is not a common_range and const V is a common_range (I know that such a range is extremely weird, but in theory, it can exist, godbolt):

#include <ranges>

struct weird_range : std::ranges::view_base {
  int* begin();
  const int* end();

  std::common_iterator<int*, const int*> begin() const;
  std::common_iterator<int*, const int*> end()   const;
};

int main() {
  weird_range r;
  auto cr = r | std::views::common;
  static_assert(std::ranges::forward_range<decltype(cr)>); // ok
  cr.front(); // ill-formed
}

In the above example, const V still satisfied the range concept, and when we apply r to views::common, its front() function will be ill-formed.

The reason is that view_interface::front() const will still be instantiated, and the common_iterator will be constructed inside the begin() const of common_view, this will cause a hard error to abort the compilation since const V itself is common_range.

Similarly, we can also create a weird range based on the same concept to make the front() of views::reverse and views::keys fail (godbolt):

#include <ranges>

struct my_range : std::ranges::view_base {
  std::pair<int, int>* begin();
  std::pair<int, int>* end();

  std::common_iterator<int*, const int*> begin() const;
  std::common_iterator<int*, const int*> end()   const;
};

int main() {
  my_range r;
  auto r1 = r | std::views::reverse;
  static_assert(std::ranges::random_access_range<decltype(r1)>); // ok
  r1.front(); // ill-formed

  auto r2 = r | std::views::keys;
  static_assert(std::ranges::random_access_range<decltype(r2)>); // ok 
  r2.front();  // ill-formed
}

So, is the const overload of begin()/end() of the range adapters underconstrained, or is the definition of weird_range itself ill-formed? Can this be considered a standard defect?


This question is mainly inspired by the LWG 3592, which states that for lazy_split_view, we need to consider the case where the const Pattern is not range, and then I subsequently submitted the LWG 3599. When I further reviewing the begin() const of other range adapters, I found that most of them only require const V to be range, this seemingly loose constraint made me raise this question.

In order to enable the range adapters' begin() const, theoretically, the constraints for const V should be exactly the same as V, which means that the long list of constraints on V, such as elements_view, needs to be replaced with const V instead of only constraints const V to be a range.

But in fact, it seems that the standard is not interested in the situation where the iterator and sentinel types of V and const V are very different.

like image 201
康桓瑋 Avatar asked Sep 21 '21 11:09

康桓瑋


1 Answers

Recent SG9 discussion on LWG3564 concluded that the intended design is that x and as_const(x) should be required to be substitutable with equal results in equality-preserving expressions for which both are valid. In other words, they should be "equal" in the [concepts.equality] "same platonic value" sense. Thus, for instance, it is not valid for x and as_const(x) to have entirely different elements.

The exact wording and scope of the rule will have to await a paper, and we'll have to take care to avoid banning reasonable code. But certainly things like "x is a range of pairs but as_const(x) is a range of ints" are not within any reasonable definition of "reasonable".

like image 115
T.C. Avatar answered Oct 15 '22 14:10

T.C.