This code compiles and works correctly:
#include <ranges>
#include <iostream>
int main() {
const auto r = std::views::iota('a', static_cast<char>('g' + 1));
for(const auto& [start, end] : r | std::views::chunk(3u)) {
for(auto it = start; it != end; ++it) {
std::cout << *it << " ";
}
std::cout << "\n";
}
return 0;
}
Its output is:
a b c
d e f
g
If I change the definition of r
as follows:
const auto r = std::views::iota('a', 'g' + 1);
The code does not compile. GCC 13 emits the following error:
chunk.cpp:13:21: error: cannot decompose inaccessible member ‘std::ranges::take_view<std::ranges::subrange<std::ranges::iota_view<char, int>::_Iterator, std::ranges::iota_view<char, int>::_Sentinel, std::ranges::subrange_kind::unsized> >::_M_base’ of ‘const std::ranges::take_view<std::ranges::subrange<std::ranges::iota_view<char, int>::_Iterator, std::ranges::iota_view<char, int>::_Sentinel, std::ranges::subrange_kind::unsized> >’
13 | for(const auto& [start, end] : r | std::views::chunk(3u)) {
| ^~~~~~~~~~~~
In file included from chunk.cpp:1:
/usr/include/c++/13/ranges:2153:11: note: declared private here
2153 | _Vp _M_base = _Vp();
| ^~~~~~~
I think that 'g' + 1
is causing integer promotion and is making the first and second parameter of iota()
have different types. However, in other occasions, this seems fine. For example, this code works as expected:
#include <ranges>
#include <iostream>
int main() {
const auto r = std::views::iota('a', 'g' + 1);
for(const auto& val : r) {
std::cout << val << " ";
}
std::cout << "\n";
return 0;
}
What is happening here? And why does the error mentions (of all things) private members?
Since 'g' + 1
produces integer promotions, this makes views::iota('a', 'g' + 1)
produce an iota_view
whose iterator type is different from the sentinel type.
According to [range.iota.sentinel], the sentinel type can only be subtracted from the iterator type when sized_sentinel_for<W, Bound>
is satisfied, which is not the case for the basic types char
and int
because they are not iterators.
In other words, this breaks iota_view
's sentinel and iterator subtraction functionality:
auto r = views::iota('a', 'g' + 1);
r.begin() - r.end(); // error
When applying views::chunck
to it, the resulting value type would be decltype(views::take(subrange(current_, end_), n_))
([range.chunk.fwd.iter]), where current_
and end_
is the iterator and sentinel of this iota_view
.
Since current_
and end_
no longer model sized_sentinel_for
, CTAD will deduce a subrange
with the third template parameter (subrange_kind
) is unsized
([range.subrange.general]), and such a subrange
is no longer sized_range
:
auto r = views::iota('a', 'g' + 1);
auto s = ranges::subrange(r.begin(), r.end());
static_assert(ranges::sized_range<decltype(s)>); // failed
This makes views::take
return a take_view
object ([range.take.overview#2.5]), which cannot be structured binding.
For your original example, since the iterator type of views::iota('a', 'g')
is the same as the sentinel type and the two can be subtracted, subrange(current_, end_)
will still model sized_range
and random_access_range
. In this case, views::take
will optimally return a subrange
type that can be structured binding ([range.take.overview#2.2.3]).
This can arguably be seen as a defect in the standard, which is also LWG 3609.