C++ 20 把 ranges standardize 之后看似有了一种很 fancy 很 fp-style 的实现算法的方式,但是这套东西坑也不少。
这篇 post 的主要内容来自 CppCon 2023 | Back to Basics: Iterators in C++ - Nicolai Josuttis,但是只总结 views/filter 贼坑的点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| #include <ranges> #include <vector>
#include "fmt/format.h" #include "fmt/ranges.h"
int main() { std::vector<int> vs{1, 4, 7, 10}; fmt::println("original = {}", vs);
auto vs_even = vs | std::views::filter([](auto&& n) { return n % 2 == 0; }); for (int& i : vs_even) { ++i; }
fmt::println("incre even 1st time = {}", vs);
for (int& i : vs_even) { ++i; }
fmt::println("incre even 2nd time = {}", vs);
for (int& i : vs_even) { ++i; }
fmt::println("incre even 3rd time = {}", vs);
for (int& i : vs_even) { ++i; }
fmt::println("incre even 4th time = {}", vs);
return 0; }
|
这四个输出分别是
1 2 3 4 5
| original = [1, 4, 7, 10] incre even 1st time = [1, 5, 7, 11] incre even 2nd time = [1, 6, 7, 11] <-- ??? incre even 3rd time = [1, 7, 7, 11] <-- ??? incre even 4th time = [1, 8, 7, 11] <-- ???
|
从第二次 incre 开始就变得不符合预期
原因是 std::views::filter 为了做到 amortized constant time,实现会在内部第一次调用 begin() 的时候缓存下第一个符合 predicate 的 iterator,而且第一次调用之后这个 begin iterator 就不变了
那怎么解决修改的问题呢?
标准给的解决方案是:你可以修改 views::filter 返回的迭代器指向的对象,但是修改之后的结果也必须符合 predicate,否则就是 undefined behavior…
你就说算不算解决吧。
所以按照标准,下面代码(也是取自上面 cppcon)就是 UB
1 2 3
| for (auto& m : monsters | std::views::filter(isDead)) { m.resurrect(); }
|
StackOverflow 上也有一个类似的讨论
比较蛋疼的一点是,这种 UB 目前看 ub sanitizer 扫不出来…