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