对于以下代码:
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++ 标准下安全?
惰性过滤的固有问题之一是,在存在突变和多遍算法的情况下,您可能会出现意想不到的行为 - 因为带有过滤器的突变,具体来说,会改变元素是否在开始的范围内!因此算法的第二遍可以获得不同数量的元素。
我之前写过的一个例子是:
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
可能会导致意想不到的行为。一般来说,这种从视图下改变范围的行为有点粗略,可能会导致常见的生命周期问题。所以只是...小心一点。