Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ranges::view::transform produces an InputIterator preventing the use of std::prev

Consider the following code, which uses the Ranges library from C++20:

#include <vector>
#include <ranges>
#include <iostream>

int main()
{
    std::vector<int> v{0,1,2,3,4,5,6,7};

    auto transformed = std::ranges::views::transform(v, [](int i){ return i * i; });

    std::cout << *std::prev(std::end(transformed));
}

I was quite surprised to learn that (at least under GCC-10.3.0 and GCC-12.0.0) this code gets stuck in std::prev.

What happens is that since the lambda doesn't return an lvalue reference, the transformed range iterators are classified as input iterators (see the rules for iterator_category selection for views::transform). However, std::prev requires the iterator to be at least a bidirectional iterator, so I guess this code is actually UB. In libstdc++ applying std::prev to an input iterator leads to this function

template<typename _InputIterator, typename _Distance>
__advance(_InputIterator& __i, _Distance __n, input_iterator_tag)
{
    // concept requirements
    __glibcxx_function_requires(_InputIteratorConcept<_InputIterator>)
    __glibcxx_assert(__n >= 0);
    while (__n--)
        ++__i;
}

being called with __n == -1, which explains the observed behavior.

If we replace std::prev with manual iterator decrement, everything works fine. Switching to std::ranges::prev works, too.

Now, it is clearly nonsensical that I can't do std::prev on what is just a view over an std::vector. While a simple solution exists, I feel extremely worried about this unexpected interplay between old and new range manipulation parts of the standard library. So, my question is: is this a known problem, and should I really forget everything not in the std::ranges namespace when working with the new ranges, and rewrite all the existing code to make sure they work with the new ranges?

like image 802
lisyarus Avatar asked Dec 30 '22 14:12

lisyarus


2 Answers

It is not a random-access-iterator by C++17's reckoning. transform must return a value rather than a reference, and C++17's iterator categories don't allow that for anything above an InputIterator.

But this type is a std::random_access_iterator by C++20's rules, which allow proxy-like iterators on any iterator/range below contiguous.

std::prev is a pre-C++20 tool, so it works by pre-C++20 rules. If you need to work with C++20 rules, you have to use the C++20 equivalent: std::ranges::prev.

Now, it is clearly nonsensical that I can't do std::prev on what is just a view over an std::vector.

No, it is necessary. C++20's conceptualized iterator categories are less restrictive than those from previous C++ versions. This means that there are iterators that cannot be used in pre-C++20 code which can be used in C++20 ranges-based code.

This is why we have new functions for these things in the ranges namespace.

like image 200
Nicol Bolas Avatar answered Jan 02 '23 02:01

Nicol Bolas


Your transform returns a prvalue, so it can't be anything other than a InputIterator. That's one of the main reasons that the iterator categories have been changed in C++20.

If the return value of your operation is a reference, then you can.

like image 36
Caleth Avatar answered Jan 02 '23 04:01

Caleth