最初的问题是为什么使用下面的代码,
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。我假设引用实现对应于本例中的标准。
1条答案
按热度按时间juzqafwq1#
这难道不是一个奇怪的要求吗?要求用户不要做一些否则会感觉很自然的事情[...]
我不这么认为,我认为例子中的代码一开始就非常奇怪,它不工作也不奇怪。
视图是短暂的。你构造你想要的视图,你使用它,然后你扔掉它。视图(很可能)会有它自己的引用依赖,你不应该在视图的生命周期内接触它们。用Rust的话来说,视图是借用构造它的容器。
考虑到这一点,构建一个
filter
,对它做一些事情,然后改变底层容器,* 然后 * 重用原始的filter
是没有意义的。为了减轻这种限制,我们必须做出什么样的妥协?为什么我们没有做出妥协?
这个限制甚至对迭代器模型来说都是相当基本的,与缓存或任何特定于范围的设计选择没有任何关系。
前向迭代器的模型是,如果你复制一个前向迭代器,然后两个都前进,那么两个副本都是有效的,并且引用同一个元素(假设它们最初不是
end()
,所以前进实际上是有效的),对于filter
也是如此:Assert保持是C++迭代器模型的一个重要部分,如果允许发生任意变化,它就不可能保持。
现在是示例中的原始迭代:
这是一种突变,打破了保证。但这是一种OK -我们正在突变,但我们突变的方式碰巧在这个上下文中没有任何不良影响。在此之后重用
colleven
肯定是不好的(因为突变打破了迭代器保证)。实际上很难准确地阐明什么情况会导致未定义的行为。但是在内部变异后循环
colleven
两次在C20范围内不起作用的事实不仅仅是缓存begin()
的结果--这是你不能允许做这类事情并维护任何迭代器保证的结果。这不是一个奇怪的要求--代码本身是有问题的。It“它只是在某种程度上有问题,这在C中是不可能诊断的。简短的版本是:视图并不打算长期存在,所以不要以这种方式使用它们。