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(); // UB! because after modification, it is no longer dead.
}

StackOverflow 上也有一个类似的讨论


比较蛋疼的一点是,这种 UB 目前看 ub sanitizer 扫不出来…