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?
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With