c++ 范围filter_view::迭代器元素修改导致UB

ne5o7dgx  于 2023-02-20  发布在  其他
关注(0)|答案(1)|浏览(111)

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

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

通过两次实体化视图,我们得到了两种不同的结果。这乍一看确实很奇怪。输出结果:
在做了一些研究和looking into potential duplicates之后,我了解到这是导致未定义行为的原因https://eel.is/c++draft/range.filter#iterator-1。
基本上,std::filter_view::iterator-和其他类似的视图-缓存开始迭代器(filter_view是从remove_if_view派生的),以实现“惰性”,从而使其保持内部状态。在特定的示例中,标准规定“即使在修改视图元素之后,用户也应该注意 * predicate 仍然为真*。”因此,我的问题现在变成:
这不是一个奇怪的要求吗?要求用户不要做一些本来会感觉很自然的事情,也就是说,两次实体化一个filter视图。为了减轻这个限制,我们必须做出哪些妥协?为什么我们没有做出这些妥协?

  • 注意 *:我的问题是关于标准视图的,我知道我链接的代码来自range-v3。我假设引用实现对应于本例中的标准。
juzqafwq

juzqafwq1#

这难道不是一个奇怪的要求吗?要求用户不要做一些否则会感觉很自然的事情[...]
我不这么认为,我认为例子中的代码一开始就非常奇怪,它不工作也不奇怪。
视图是短暂的。你构造你想要的视图,你使用它,然后你扔掉它。视图(很可能)会有它自己的引用依赖,你不应该在视图的生命周期内接触它们。用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

Assert保持是C++迭代器模型的一个重要部分,如果允许发生任意变化,它就不可能保持。
现在是示例中的原始迭代:

for (int& i : colleven) {
    i += 1;
}

这是一种突变,打破了保证。但这是一种OK -我们正在突变,但我们突变的方式碰巧在这个上下文中没有任何不良影响。在此之后重用colleven肯定是不好的(因为突变打破了迭代器保证)。实际上很难准确地阐明什么情况会导致未定义的行为。
但是在内部变异后循环colleven两次在C20范围内不起作用的事实不仅仅是缓存begin()的结果--这是你不能允许做这类事情并维护任何迭代器保证的结果。这不是一个奇怪的要求--代码本身是有问题的。It“它只是在某种程度上有问题,这在C中是不可能诊断的。
简短的版本是:视图并不打算长期存在,所以不要以这种方式使用它们。

相关问题