范围 filter_view::iterator 元素修改导致 UB

joh*_*lis 5 c++ iterator range-v3 c++20 std-ranges

最初的问题是为什么使用以下代码,

std::vector<int> coll{1,4,7,10};

auto iseven = [](auto&& i){return i % 2 == 0; };
auto  colleven = coll | std::views::filter(iseven);

// first view materialization
for(int& i : colleven)
{
    i += 1;
}
for(auto i : coll)
std::cout << i << ' ';
std::cout << std::endl;

// second view materialization
for(int& i : colleven)
{
    i += 1;
}
for(auto i : coll)
std::cout << i << ' ';
Run Code Online (Sandbox Code Playgroud)

通过两次具体化视图,我们得到两个不同的结果。乍一看这确实很奇怪。输出:

1 5 7 11 
1 6 7 11 
Run Code Online (Sandbox Code Playgroud)

经过一些研究并研究潜在的重复项后,我了解到这是https://eel.is/c++draft/range.filter#iterator-1的未定义行为的原因。

基本上,std::filter_view::iterator和其他类似的视图一样,缓存开始迭代器(filter_view 派生自remove_if_view),以实现“惰性”,从而保持内部状态。在特定示例中,标准规定“即使在修改视图元素之后,用户也应该注意谓词保持为真”。所以我的问题现在变成:

这不是一个奇怪的要求吗?要求用户不要做一些看起来很自然的事情,即filter两次具体化视图。为了减轻这种限制,我们必须做出哪些妥协?为什么我们没有做出这些妥协?

注意:我的问题涉及标准视图,我知道我链接的代码来自 range-v3。我认为参考实现与本例中的标准相对应。

Bar*_*rry 6

更新:我在博客文章中写了这个答案的更长版本:通过过滤器进行变异


这不是一个奇怪的要求吗?要求用户不要做一些原本感觉很自然的事情[...]

我不这么认为。我认为示例中的代码实际上一开始就非常奇怪,并且它不起作用也就不足为奇了。

视图是短暂的。您构建所需的视图,使用它,然后丢弃它。视图(可能)将具有自己的引用依赖项,并且您不应该视图的生命周期内触及它们。用 Rust 的术语来说,视图是借用构建它的容器。

考虑到这一点,构造一个filter,用它做一些事情,然后改变底层容器,然后重新使用原始的是没有意义的filter。只需构建一个新的即可。

为了减轻这种限制,我们必须做出哪些妥协?为什么我们没有做出这些妥协?

呃,没有。即使对于迭代器模型来说,这种限制也是相当基本的,并且与缓存或任何特定于范围的设计选择无关。

前向迭代器的模型是,如果复制一个前向迭代器,然后将两个副本都前进,则两个副本都是有效的并且引用相同的元素(假设它们最初不是这样,end()因此前进实际上是有效的)。filter这也同样适用:

vector<int> v = {1, 2, 3, 4};
auto f = v | views::filter(iseven);
auto it = f.begin(); // this is the 2
auto it2 = it;       // this is also the 2
++it;                // this is the 4
*it = 5;
++it2;               // oops: this is v.end()
assert(it == it2);   // nope
Run Code Online (Sandbox Code Playgroud)

该断言保持是 C++ 迭代器模型的重要组成部分,如果允许发生任意突变,则它不可能保持。

现在是示例中的原始迭代:

for (int& i : colleven) {
    i += 1;
}
Run Code Online (Sandbox Code Playgroud)

这会导致破坏保证的突变。但这没关系——我们正在变异,但我们的变异方式恰好在这种情况下不会产生任何不良影响。此后重用colleven绝对不行(因为突变破坏了迭代器保证)。实际上很难准确地阐明哪些情况会导致未定义的行为。

colleven但是,内部突变后循环两次在 C++20 范围中不起作用的事实不仅仅是缓存begin()的结果 - 这是您不允许执行此类操作并维护任何迭代器这一事实的结果保证。这并不是一个奇怪的要求——代码本身就有问题。这只是在 C++ 中无法诊断的问题。

简而言之:视图并不打算长期存在,所以不要以这种方式使用它们。