直接从filter_view的底层范围中删除元素是未定义的行为吗?

问题描述 投票:0回答:1

对于以下代码:

void foo(std::set<int>& s) {
    for (int val : s | std::views::filter([](int i) { return i > 0; })) {
        s.erase(val - 3);
    }
}

此代码无意执行任何有意义的任务;这仅用于说明目的。在这里,我使用范围适配器

std::views::filter
过滤掉
s
中的所有正数,并删除相应的
val - 3
元素。

此代码涉及在迭代容器时从容器中删除元素。对于

std::set
,擦除元素不会使除指向已擦除元素之外的迭代器无效。因此,如果我将上面的代码转换为等效的迭代器形式,它不应该包含未定义的行为:

void bar(std::set<int>& s) {
    for (int val : s) {
        if (val > 0) {
            s.erase(val - 3);
        }
    }
}

但是,我不确定

foo
是否包含未定义的行为,因为该标准对范围库中的某些概念施加了额外的语义约束,例如要求约束表达式保持相等。我不确定
foo
是否违反了这些语义要求。

此外,range.filter.iterator明确允许通过

filter_view
中的迭代器修改底层元素,只要修改后的元素仍然满足谓词即可。但该标准没有指定直接修改基础范围时会发生什么,如
foo
所示。

这些修改导致

foo
未定义行为,还是在当前 C++ 标准下安全?

c++ language-lawyer std c++20 std-ranges
1个回答
0
投票

惰性过滤的固有问题之一是,在存在突变和多遍算法的情况下,您可能会出现意想不到的行为 - 因为带有过滤器的突变,具体来说,会改变元素是否在开始的范围内!因此算法的第二遍可以获得不同数量的元素。

我之前写过的一个例子是:

vector<int> v = {1, 2, 3, 4, 5, 6}; auto evens = v | views::filter(is_even); pairwise(evens.begin(), evens.end(), [](int& i, int& j){ fmt::println("({}, {})", i, j); ++j; });
打印

(2, 4)

然后...
(6, 6)
。因为尾随迭代器会跳过一个元素(因为它不再满足谓词)并赶上前导迭代器。

但是

filter

的主要问题是
具体关于将元素从满足谓词更改为不满足谓词。实际上,从范围下方删除元素(正如您所做的那样)并不是真正特定于 views::filter
——它与从 
views::transform
 中的范围下方删除元素没有什么不同。只是我们想说,这种特殊形式的突变对于 
views::filter
 可能会导致意想不到的行为。

一般来说,这种从视图下改变范围的行为有点粗略,可能会导致常见的生命周期问题。所以只是...小心一点。

© www.soinside.com 2019 - 2024. All rights reserved.