Ranges 把「两个迭代器表示一段序列」提升为一等公民:range 是有
begin()/end()(或满足std::ranges::range概念)的对象;views 在管道里惰性变换;range 算法 统一用整段序列调用。本文面向已经会写
for (auto it = v.begin(); it != v.end(); ++it)的读者,讲清核心心智模型、常用视图、与算法配合的写法,以及几个真实项目里最容易踩的坑。
1. 为什么要有 Ranges?
经典 STL 写法是「算法 + 两个迭代器」:
cpp
std::sort(v.begin(), v.end());
auto it = std::find_if(v.begin(), v.end(), [](int x) { return x % 2 == 0; });
问题不在「能不能用」,而在于:
- 重复写
begin/end,噪声大,也容易把两个容器的迭代器配错对。 - 中间结果常要落到临时容器里,才能继续链式处理,既冗长又可能多一次分配。
- 组合逻辑不直观:「先过滤再映射再取前 N 个」在循环或多次算法调用里读起来费劲。
Ranges 的方向是:
- 算法 接受「整段 range」,例如
std::ranges::sort(v)。 - 视图(view)描述一种惰性的序列变换:在遍历时才算下一个元素,通常不拥有底层数据。
- 管道
|把视图串起来,读起来接近数据流。
cpp
#include <ranges>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v{1, 2, 3, 4, 5, 6};
auto r = v
| std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * x; });
for (int x : r) std::cout << x << ' '; // 4 16 36
}
2. 三个词先分清:Range、View、Adaptor
| 术语 | 直觉 | 备注 |
|---|---|---|
| Range | 「能从头到尾遍历的东西」 | vector、string、span、视图本身,只要满足 std::ranges::range。 |
| View | 一种轻量、通常不拥有数据的 range | 复制应便宜;惰性求值;很多在 std::views:: 里。 |
| Range adaptor | 把管道左侧「再包一层」的函数对象 | 例如 std::views::filter(pred) 返回 adaptor,`rng |
惰性 的含义:上面例子里的 filter + transform 不会在 r 构造时扫完整张 vector;只有你迭代 r(或交给需要遍历的算法)时,才会按需从 v 里拉取元素。
3. 头文件与命名空间习惯
- 核心定义在
<ranges>;很多算法有 range 重载 ,在<algorithm>里,位于std::ranges::。 - 视图工厂一般在
std::views::(就是std::ranges::views::的别名)。
典型写法:
cpp
#include <ranges>
#include <algorithm>
#include <vector>
void demo(std::vector<int>& v) {
std::ranges::sort(v);
auto w = v | std::views::reverse;
(void)w;
}
4. 常用视图(C++20 为主,顺带 C++23)
下面只列日常最高频 的一批;完整列表以 cppreference · Range adaptors 为准。
4.1 filter / transform
过滤与映射,相当于惰性版「先筛再映」:
cpp
auto r = vec
| std::views::filter([](const auto& x) { return x > 0; })
| std::views::transform([](int x) { return x * 2; });
4.2 take / drop
只取前 N 个、丢掉前 N 个:
cpp
auto first3 = v | std::views::take(3);
auto rest = v | std::views::drop(3);
4.3 keys / values(map 上很好用)
cpp
#include <map>
#include <ranges>
void print_keys(const std::map<int, std::string>& m) {
for (int k : m | std::views::keys)
std::cout << k << '\n';
}
4.4 iota / iota + take
生成无限递增序列的视图,务必 用 take 等截断,否则自己 for 循环别写错条件:
cpp
for (int i : std::views::iota(1, 11)) // [1, 11)
std::cout << i << ' ';
4.5 zip(C++23)
并行遍历多个 range(长度取最短):
cpp
// C++23
for (auto [a, b] : std::views::zip(xs, ys))
use(a, b);
若你仍在 C++20,可以用手写索引或第三方实现;标准库要到 23 才补齐。
4.6 split / lazy_split(按分隔「切」视图)
适合简单协议解析、按字符切分等;注意返回的是子 range 的视图,底层仍是原字符串的生命周期。
5. Range 版算法:少写一对迭代器
很多算法提供 std::ranges::xxx(rng, ...) 形式,例如:
cpp
std::ranges::sort(v);
if (std::ranges::find(v, 42) != v.end()) { /* ... */ }
带投影(projection)的重载(C++20)可以把「比较键」从谓词里拆出来,例如按结构体某一字段排序:
cpp
struct Person { std::string name; int age; };
std::vector<Person> people;
std::ranges::sort(people, {}, &Person::age); // 升序按 age
std::ranges::sort(people, std::greater{}, &Person::age); // 降序
第三个参数是 projection :在比较前先「投影」成参与比较的值。比手写 lambda 比较字段更短、更不容易写错。
6. 视图什么时候要「物化」?
视图不拥有数据,很多算法又需要「有明确端点的序列」或对同一序列遍历多次。此时用 std::ranges::to(C++23)最干净:
cpp
#include <ranges>
#include <vector>
std::vector<int> materialize(auto&& r) {
return r | std::ranges::to<std::vector<int>>();
}
C++20 可以退而求其次:std::vector<int> out; std::ranges::copy(r, std::back_inserter(out)); 或手写 reserve + assign。
经验法则:
- 只遍历一次、且逻辑简单 → 保持 view,省钱。
- 需要
size()、随机访问、多次遍历、或传给只认具体容器的 API → 物化 到vector等。
7. 生命周期与悬空:Ranges 的头号坑
视图里往往存的是引用或迭代器指向外部数据。下面这种模式是未定义行为:
cpp
auto bad() {
std::string s = "hello world";
return s | std::views::split(' ');
} // s 已销毁,调用方迭代返回的 view → 悬空
// 正确:让拥有者的生命周期覆盖 view 的使用期
std::string s = "hello world";
auto tokens = s | std::views::split(' ');
for (auto t : tokens) { /* 使用 t,注意 t 也是 subrange */ }
另一常见坑是 join :若外层 range 里的内层 range 是临时的,join 后迭代也可能悬空。解决办法同样是:保证每一层被 join 的 range 都活着,或先物化成稳定容器。
原则 :把 views 看成「悬挂在别物上的窗口」------窗口本身很轻,但被看的东西不能先没了。
8. 与「手写 for」如何取舍?
| 场景 | 更倾向 |
|---|---|
| 单次线性处理、条件分支多、需要早退与复杂状态 | 手写循环往往更清晰 |
| 数据管线清晰、变换可命名、复用多 | views + ranges:: 算法可读性更好 |
| 性能敏感热点路径 | 实测为准;view 组合有时增加间接层,有时又省掉中间容器 |
Ranges 不是「取代所有循环」,而是给声明式、可组合的序列处理多一套一等工具。
9. 小结
- Range 统一了「一整段序列」的表达;view 提供惰性、可管道的变换。
rng \| std::views::xxx读起来像 Unix 管道,适合过滤 / 映射 / 截取 / 配对等组合逻辑。std::ranges::算法减少迭代器噪音,投影参数能简化按字段排序/查找。- 生命周期 :视图不延长被引用对象的生命周期;跨函数返回 view 要特别小心;需要稳定迭代或多次遍历时用物化。
把 Ranges 用熟之后,再读 <ranges> 里概念分层(range、view、borrowed_range 等)会轻松很多;日常写业务代码,掌握本文几类视图 + range 算法 + 生命周期意识,就足够应付大部分场景。