ranges::view::transform 生成一个 InputIterator 以防止使用 std::prev

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

考虑以下代码,它使用了 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));
}

我很惊讶地得知(至少在 GCC-10.3.0 和 GCC-12.0.0 下)这段代码 gets stuck in std::prev

发生的事情是,由于 lambda 没有 return 左值引用,transformed 范围迭代器被归类为输入迭代器(参见 rules for iterator_category selection for views::transform). However, std::prev requires 迭代器位于至少是一个双向迭代器,所以我猜这段代码实际上是 UB。在 libstdc++ 中,将 std::prev 应用于输入迭代器会导致此函数

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;
}

被调用 __n == -1,这解释了观察到的行为。

如果我们用手动迭代器递减替换std::preveverything works fine. Switching to std::ranges::prev works, too

现在,我不能对 std::vector 的观点进行 std::prev 显然是荒谬的。虽然存在一个简单的解决方案,但我对标准库的新旧范围操作部分之间这种意想不到的相互作用感到非常担心。所以,我的问题是:这是一个已知问题吗?在使用新范围时,我是否真的应该忘记不在 std::ranges 命名空间中的所有内容,并重写所有现有代码以确保它们正常工作使用新范围?

您的变换 return 是一个纯右值,因此它 不能 除了 InputIterator 以外的任何东西。这是迭代器类别在 C++20 中发生变化的主要原因之一。

如果你操作的return值为参考,then you can

根据 C++17 的计算,它不是随机访问迭代器。 transform 必须 return 一个值而不是 reference,并且 C++17 的迭代器类别不允许在 InputIterator 之上的任何东西。

但是根据 C++20 的规则,这种类型是 std::random_access_iterator,它允许在连续的 iterator/range 下面的任何 iterator/range 上使用类似代理的迭代器。

std::prev 是 C++20 之前的工具,因此它按照 C++20 之前的规则工作。如果您需要使用 C++20 规则,则必须使用 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.

没有,有必要。 C++20 的概念化迭代器类别比以前的 C++ 版本中的迭代器类别限制更少。这意味着有些迭代器不能在 C++20 之前的代码中使用,而 可以 在基于 C++20 范围的代码中使用。

这就是我们在 ranges 命名空间中为这些东西提供新功能的原因。