3

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?

Alberto Santini
  • 6,425
  • 1
  • 26
  • 37
  • 1
    Not a complete answer,, but hopefully a sth helpful: depending on the conversion to char the inner range of chunked becomes sized (for char) or unsized (for char-int pair). Not sure if that is correct behaviour here. https://godbolt.org/z/r7Tb9KxEh – alagner Jul 15 '23 at 12:23
  • 1
    I too think 'g' + 1 is promoting to int. And since views are templates no implicit conversion (back to) char is considered. And that's why you need the explicit conversion. – Pepijn Kramer Jul 15 '23 at 12:25
  • @PepijnKramer it is promoting for sure. But the problem is contextual and manifests itself when taking range and passing it to another adapter. With just `iota` it works fine and the promotion does not break anything; moreover the resulting range is always sized. – alagner Jul 15 '23 at 12:32
  • Seems to me that it all stems from the fact `sized_sentinel_for` in case of char-int pair is unsatisfied. The question is though, whys `size()` is avaialable for it, see: https://godbolt.org/z/YKGWMqbPY – alagner Jul 15 '23 at 12:59
  • @alagner "*The question is though, whys size() is avaialable for it*" Because `views::iota('a', int('g' + 1))` has a `size()` member. – 康桓瑋 Jul 15 '23 at 13:41

1 Answers1

6

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.

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