59 / 80 · C++20 · 约 10 分钟
Ranges 与惰性 views:管道不是结果缓存
C++20 ranges 把范围作为算法输入,views 可组合筛选、映射等惰性操作。创建管道通常没有计算全部结果,重复遍历也不承诺复用结果;理解底层所有权、迭代能力与最终物化边界比管道语法更重要。
范围接口与视图是两件事
range 表示能取得开始与结束的序列,结束位置还可以由不同类型的 sentinel 表达。ranges 算法接受整个范围,并用概念约束所需的迭代能力。view 则是一类适合低成本移动与组合的范围类型,并不等于容器,也不必然等于完全不拥有数据。
有些 view 借用现有容器,有些按需生成值,有些形式可以拥有底层对象。名称里有 view 不能单独回答生命周期问题。示例从局部 vector 左值构造管道,因此管道借用该容器;容器在整个遍历期间都存活且没有发生使迭代器失效的结构修改。
filter 与 transform 在消费时工作
values | views::filter(pred) | views::transform(fn) 描述如何访问结果:迭代推进时筛选,解引用相关迭代器时转换。它一般不立即分配一个结果数组,也不保存全部转换结果。因此不能把创建 view 的时间当成完成整个算法的时间。
重复遍历可能重新执行转换,有些视图还会缓存迭代状态而不是缓存结果值。应尽量给谓词与转换函数提供稳定、符合语义要求的行为,避免用打印次数或全局计数器推断可移植的精确调用次数;有副作用的管道尤其需要审查。
需要快照时明确物化
例子筛选偶数再求平方,最后通过一次循环放入 vector。这一步明确形成拥有结果的快照,之后原始输入即使改变,也不会改变结果容器。C++20 示例不使用 C++23 的 ranges::to,避免把较新库功能误标为基线能力。
惰性并非总更快:多次消费昂贵转换时,物化一次可能更经济;只取很短前缀时,惰性则可能省去大量无用计算。filter 还可能削弱随机访问能力,不能假定所有容器算法都能直接作用于任意管道。选型时同时考虑算法需求、生命期和访问次数。
容易答错的地方
- 返回借用局部 vector 的 view 会悬空;管道对象活着不代表它引用的数据也活着。
- 不要在遍历 filter_view 时随意修改元素使其不再满足谓词,或执行使底层迭代器失效的操作;惰性筛选有自己的有效性要求。
运行一个例子
最低标准 C++20 · 完整程序 · 下载 .cpp
#include <cassert>
#include <iostream>
#include <ranges>
#include <vector>
int main() {
std::vector<int> values{1, 2, 3, 4, 5, 6};
auto selected = values
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; });
std::vector<int> result;
for (int n : selected) result.push_back(n);
assert((result == std::vector<int>{4, 16, 36}));
values[1] = 8;
assert(result[0] == 4);
std::cout << result[0] << ' ' << result[1] << ' ' << result[2] << '\n';
}
在本地编译
g++ -std=c++20 -Wall -Wextra -Wpedantic -pthread modern-ranges.cpp -o example && ./example预期结果
4 16 36
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
希望只保留筛选后前两个平方,应把 views::take(2) 放在哪?把它放在整个管道最前面会怎样?
查看参考答案
放在现有 filter 与 transform 之后,结果是 4、16。放在最前面会先截取输入的 1、2,再筛选偶数,只剩 4。管道顺序表达业务顺序,take 操作的对象是当前阶段的序列,不是总能自动理解成“最终结果数量”。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。