The problem of the definition of transform_view​::iterator's iterator_category?

Viewed 163

The standard defines a variety of range adaptors in [range.adaptors], and some of them have their own iterator types.

In order to standardize the iterator_category of these iterators, the standard also specifies how they are defined. For example, in [range.transform.iterator-2], the iterator_category of transform_view​::​iterator is defined as follows:

The member typedef-name iterator_­category is defined if and only if Base models forward_­range. In that case, iterator​::​iterator_­category is defined as follows: Let C denote the type iterator_­traits<iterator_­t<Base>>​::​iterator_­category.

If is_­lvalue_­reference_­v<invoke_­result_­t<F&, range_­reference_­t<Base>>> is true, then

  • if C models derived_­from<contiguous_­iterator_­tag>, iterator_­category denotes random_­access_­iterator_­tag;

  • otherwise, iterator_­category denotes C.

Otherwise,iterator_­category denotes input_­iterator_­tag.

But this definition seems to have some problems, consider the following case:

vector v{1, 2, 3, 4, 5};
auto r = views::iota(0, 5) | 
         views::transform([&](int i) -> int& { return v[i]; });

using I = ranges::iterator_t<decltype(r)>;
static_assert(random_access_iterator<I>);
static_assert(same_as<I::iterator_concept, random_access_iterator_tag>);

static_assert(__detail::__cpp17_randacc_iterator<I>);
static_assert(same_as<I::iterator_category, input_iterator_tag>);

Since r is perfect random_access_range in C++20, its iterator models random_access_iterator, however, according to the description above, since iota_view::iterator_category is only input_iterator_tag, the iterator_category of iterator I is also only input_iterator_tag.

But obviously, iterator I meets all the requirements of LegacyRandomAccessIterator, so it should be more reasonable if its iterator_category is random_access_iterator.

Is this a standard defect? Or is there a consideration behind this?

1 Answers

According to C++20, the iterator category for a transform_view is determined as follows:

iterator​::​iterator_­category is defined as follows: Let C denote the type iterator_­traits<iterator_­t<Base>>​::​iterator_category.

  • If is_­lvalue_­reference_­v<invoke_­result_­t<F&, range_reference_­t<Base>>> is true, then
    • if C models derived_­from<contiguous_­iterator_­tag>, iterator_­category denotes random_­access_­iterator_­tag;
    • otherwise, iterator_­category denotes C.
  • Otherwise, iterator_­category denotes input_­iterator_tag.

So if your functor returns an lvalue reference, then the iterator category is derived from the iterator category of the base iterator type. And the base range that provides the base iterator is views::iota.

However, because iota is a prvalue range (its iterators return prvalues, not references), its iterator_category cannot be anything other than std::input_iterator_tag. So in the above text for transform_view, C will be std::input_iterator_tag. So the category for transform_view will be std::input_iterator_tag, no matter what your functor returns.

You should really avoid caring about iterator_category when you're using std::ranges-based code. If you know you're dealing with C++20 ranges, then you should be using the iterator_concept.

Related