Search code examples
c++c++20std-rangesc++23

Why do I get an error: cannot decompose inaccessible member when compiling this code using std::views::chunk?


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?


Solution

  • 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.