C++20 Ranges:告别 begin/end
C++20 Ranges:告别 begin/end
C++98 以来的 STL 算法库一直有一个尴尬的体验:算法威力强大,但调用方式令人难受——std::sort(v.begin(), v.end()) 写了几十年。当你需要把"筛选、变换、去重、排序"组合起来时,每次中间操作都要分配新容器,或者把 begin/end 反复传递。
C++20 Ranges 彻底改变了这一局面。它把 STL 从"基于迭代器对"升级为"基于范围"的编程模型,并引入 views 管道——链式调用、懒惰求值、零拷贝中间结果。
1. Range 概念:一切范围的基础
Ranges 库的核心抽象是 std::ranges::range 概念:
template<typename T>
concept range = requires(T& t) {
std::ranges::begin(t); // 可以获取起始迭代器
std::ranges::end(t); // 可以获取终止哨兵
};任何满足 range 的类型都可以直接用 Ranges 算法:
std::vector<int> v = {3, 1, 4, 1, 5};
std::ranges::sort(v); // 不再需要 v.begin(), v.end()
auto result = std::ranges::find(v, 4); // 返回迭代器这就是最直观的变化——范围是传递给算法的自然单位。
2. View 与 View Adaptor:管道式编程
View 是一个轻量级的、懒惰求值的范围。它的关键性质:
- O(1) 拷贝/移动/析构
- 懒惰求值:迭代时才计算元素
- 不拥有数据:是对底层 range 的"视角"
View Adaptor 是"接受一个 range,返回一个 view"的函数。管道运算符 | 让它们可以串联:
namespace rv = std::ranges::views;
std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto result = v
| rv::filter([](int x) { return x % 2 == 0; }) // 筛出偶数
| rv::transform([](int x) { return x * x; }) // 平方
| rv::take(3) // 只取前 3 个
| rv::reverse; // 反转
// result = [100, 36, 16],每个元素在迭代时才计算
// 转回具体容器
auto vec = result | std::ranges::to<std::vector<int>>(); // C++23
// C++20 的替代写法:
std::vector<int> vec2(result.begin(), result.end());这串管道没有产生任何中间 std::vector。每个元素从原始数据开始,依次经过 filter → transform,直到被消费才完整计算一步。
3. 常用 views 速览
3.1 过滤与变换
rv::filter(pred) // 保留满足 pred 的元素
rv::transform(f) // 对每个元素应用 f
rv::take(n) // 只取前 n 个
rv::drop(n) // 跳过前 n 个
rv::take_while(pred) // 取到第一个不满足 pred 的为止
rv::drop_while(pred) // 跳过满足 pred 的前缀3.2 结构变化
rv::join // 把 range-of-range 展平为一层
rv::split(delim) // 按分隔符切分
rv::concat(r1, r2) // C++26: 拼接两个同类型 range3.3 索引与枚举
rv::enumerate // C++23: [(0, e1), (1, e2), ...]
rv::zip(v1, v2) // C++23: [(v1[0], v2[0]), ...]
for (auto [idx, value] : v | rv::enumerate) {
std::cout << idx << ": " << value << '\n';
}3.4 生成型
rv::iota(0, 10) // [0, 1, 2, ..., 9]
rv::repeat(x, n) // C++23: [x, x, ..., x] n 次4. Projection:Ranges 最被低估的特性
Ranges 算法的每个重载都接受一个额外的 projection 参数——一个在比较/操作前应用于元素的变换:
struct Person { std::string name; int age; };
std::vector<Person> people = {{"Alice", 30}, {"Bob", 25}, {"Charlie", 35}};
// 按年龄排序——不需要手写 lambda 提取 age!
std::ranges::sort(people, {}, &Person::age);
// ↑ ↑
// 比较器(默认<) projection: 排序前把元素映射为 age
// 按姓名长度排序
std::ranges::sort(people, std::less{}, [](Person const& p) {
return p.name.size();
});Projection 的设计解决了一个古老问题:"我想按某个成员的某个属性比较,但我不想写一个完整的比较器。" 它把"提取比较依据"和"比较"两个关注点彻底分离。
projection 的执行机制
projection 对每个元素只调用一次,结果被缓存。在排序这种 O(n log n) 次比较的场景下,这是巨大的性能节约——没有 projection 的话,你写的 lambda 需要在比较器中被调用 O(n log n) 次。
5. Views 的 Lazy 语义与陷阱
Lazy 求值是 views 的核心能力,也是核心陷阱来源:
5.1 悬空引用
auto dangerous() {
std::vector<int> v = {1, 2, 3, 4, 5};
return v | rv::filter([](int x) { return x > 2; });
} // v 析构!返回的 view 持有 v 的引用 → 悬空View 不拥有数据。如果底层 range 是临时的,view 必须随之一同消亡。C++20 禁止了把临时容器 pipe 给 view(通过 borrowed_range 机制),但把 view 从函数返回时,编译器无法检查。
5.2 多次遍历
auto filtered = v | rv::filter([](int x) { return x % 2 == 0; });
auto n1 = std::ranges::distance(filtered); // 第一次遍历
auto n2 = std::ranges::distance(filtered); // 第二次遍历,OK(v 还在)filter 等 view 是 input range——它们不支持 random access。每次遍历都会重新计算谓词。
5.3 缓存(Caching)
有些 view(如 filter)需要缓存已计算的元素位置(begin() 计算后需要记住),这导致它们不是 const-iterable 的:
auto const filtered = v | rv::filter(pred);
// filtered.begin(); // 可能编译错误!filter view 的 begin() 可能不是 const 的6. Ranges 算法的优势:ADL 隔离 + 安全增强
std::ranges::sort() 不仅仅是 std::sort() 的 begin/end 版本。Ranges 算法提供了多重增强:
- Concept-based 重载决议:传错类型时错误信息更清晰。
- borrowed_range 检查:返回 dangling 迭代器时,
std::ranges::find在临时对象上会编译错误。 - Projection 支持(见上文)。
- 统一的返回值结构:
std::ranges::minmax返回struct { T min; T max; }而不是 STL 的std::pair。
7. 从 C++20 到 C++23
C++23 对 Ranges 做了大幅增强:
| 特性 | 说明 |
|---|---|
ranges::to<T>() | 将 view 直接转回具体容器 |
views::enumerate | 带索引遍历 |
views::zip / zip_transform | 多容器并行遍历 |
views::chunk / slide | 按固定大小分组 / 滑动窗口 |
views::stride | 每隔 n 个元素取一个 |
views::cartesian_product | 笛卡尔积 |
总结
Ranges 不是 STL 算法的"begin/end 省略版",而是一次编程模型升级:
- 管道组合替代中间容器和中间变量
- 懒惰求值避免不必要的内存分配
- Projection 分离"比较什么"和"怎么比较"
- Concept 约束让错误在调用方暴露,而非算法内部
当你下次写 std::sort(v.begin(), v.end(), [](const T& a, const T& b){ return a.member < b.member; }) 时,想想 std::ranges::sort(v, {}, &T::member)——这不仅是字符数的减少,而是意图的显式化。