【C++20】(七)Ranges 库:算法即表达式,管道即组合
STL 算法 + Ranges 视图 = 代码即管道,数据流即表达式。
前言
你写过这样的代码吗?
1 | std::vector<int> nums = {1,2,3,4,5,6,7,8,9,10}; |
五层嵌套,变量传来传去,调试的时候根本不知道数据流向了哪里。
C++20 Ranges 库用管道操作符(|)重新定义了算法组合方式,让代码变成数据流的声明式描述。
一、Ranges 核心概念
1.1 View:惰性求值的序列
Ranges 的核心是 View——一种惰性求值(Lazy Evaluation)的序列:
- 不像容器一次性分配所有内存
- 按需计算,只有迭代时才产生元素
- 可以无限(Infinite),如
iota
flowchart LR
subgraph "传统容器"
A["📦 容器 [1,2,3,4,5]"] --> B["复制全部数据"]
end
subgraph "Ranges View"
C["🔗 View(视图)"] --> D["按需计算,惰性求值"]
end
style A fill:#FFB3C6,stroke:#F48FB1,color:#333
style B fill:#FFDAB9,stroke:#FFAB76,color:#333
style C fill:#B5EAD7,stroke:#80CBC4,color:#333
style D fill:#C7CEEA,stroke:#9FA8DA,color:#3331.2 管道操作符:|
Ranges 最大的创新是管道操作符 |,将多个操作串联成链:
1 | // 原:嵌套调用 |
读法:数据从左向右”流”过每个变换,像 shell 的管道。
二、常用视图操作
1 |
|
2.1 filter:过滤
1 | auto evens = nums | rv::filter([](int n){ return n % 2 == 0; }); |
2.2 transform:转换
1 | auto squared = nums | rv::transform([](int n){ return n * n; }); |
2.3 take / drop:截取
1 | auto first5 = nums | rv::take(5); // 前5个 |
2.4 reverse:反转
1 | auto reversed = nums | rv::reverse; |
2.5 iota:无限序列
1 | // 生成无限序列:1, 2, 3, 4, 5, ... |
2.6 all + common:兼容旧代码
1 | // std::views::all 将容器转为 view |
三、视图组合示意
flowchart LR
N["📥 [1,2,3,4,5,6,7,8,9,10]"]
F["🔍 filter(偶数)"]
T["✨ transform(平方)"]
K["📏 take(3)"]
O["📤 [4, 16, 36]"]
N --> F --> T --> K --> O
style N fill:#C7CEEA,stroke:#9FA8DA,color:#333
style F fill:#E8D5F5,stroke:#CE93D8,color:#333
style T fill:#FFDAB9,stroke:#FFAB76,color:#333
style K fill:#B5EAD7,stroke:#80CBC4,color:#333
style O fill:#FFB3C6,stroke:#F48FB1,color:#333四、Ranges vs 传统 STL
| 维度 | 传统 STL | Ranges |
|---|---|---|
| 组合方式 | 嵌套函数调用 | 管道操作符 ` |
| 求值时机 | 即时(容器) | 惰性(视图) |
| 代码风格 | 命令式 | 声明式 |
| 可读性 | 一般 | ✅ 优秀 |
| 性能 | 即时分配 | ✅ 按需计算 |
| 适用场景 | 简单操作 | 复杂数据流 |
五、实际应用场景
5.1 数据清洗管道
1 | auto clean_data = raw_data |
5.2 无限序列处理
1 | // 生成斐波那契数列 |
5.3 字符串处理
1 | using namespace std::ranges::views; |
六、注意事项
- Views 不存储数据:迭代器失效后 view 不可用
- 不是所有操作都返回 view:
to_vector等终端操作会实例化容器 - 编译时间:复杂管道会增加编译时间,但运行时性能通常更好
结论与建议
Ranges 让数据流变成了代码的第一公民,
|操作符将算法串联成可读性极高的管道。
什么时候用 Ranges:
- 数据处理链超过 2 个步骤
- 需要惰性求值避免不必要计算
- 无限序列操作
什么时候用传统 STL:
- 简单的一次性操作
- 需要存储结果供后续使用
- 编译时间敏感的场景
下一步:学习 std::ranges::to(C++23 将容器转换写为 vec | to<std::vector>),以及 Ranges 与协程的结合使用。
记住:Ranges 不是替代 STL 算法,而是增强。你的
std::sort、std::accumulate依然有效,只是数据流的上游可以使用 Ranges 优雅地组合。
📚 C++20 新特性 系列导航
本文是《C++20 新特性》系列第 7/7 篇。
| 方向 | 章节 |
|---|---|
| ◀ 上一篇 | (六)Coroutine 协程 |
📖 全部 7 篇目录(点击展开)
本章讲 C++20 的 Ranges 库(views、actions、pipeline),对比维度:本特性 vs C++17 algorithm 旧写法 vs 其他语言流式数据处理。
对比分析
一、本特性 vs 旧写法
| 维度 | C++20 ranges | C++17 algorithm | 影响 |
|---|---|---|---|
| 写法 | v | filter(...) | transform(...) | take(5) | 嵌套 std::copy_if / std::transform | 流水线 |
| 惰性 | 默认惰性求值 | 立即 | 性能 + 表达力 |
| 自定义 view | 自定义 view + 适配器 | 需手写迭代器 | 易扩展 |
| 范围概念 | 统一 range 概念 + iterator 概念 | 无统一抽象 | 泛型更强 |
二、对比其他语言
| 语言 | 流式数据处理 | 风格 |
|---|---|---|
| C++20 | ranges + views | 惰性、编译期 |
| Python | itertools / pandas | 解释、惰性或急切 |
| Java | Stream API | 急切 + parallel |
| Rust | Iterator trait + 适配器 | 惰性、零开销抽象 |
| LINQ (C#) | LINQ | 解释 / 编译双模式 |
三、优缺点
优点:
- 把”流水线”写成”读起来像数据流”
- 惰性 + 零开销抽象
缺点:
- 学习成本:views 数量多
- 自定义 view 的模板代码仍较复杂
四、何时选
- 数据处理管线:ranges
- 简单过滤:algorithm 仍可
- 大数据并行:考虑 execution::par(C++17 起并行算法)