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.
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".
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With