【C++20】(七)Ranges 库:算法即表达式,管道即组合

STL 算法 + Ranges 视图 = 代码即管道,数据流即表达式。

前言

你写过这样的代码吗?

1
2
3
4
5
6
7
std::vector<int> nums = {1,2,3,4,5,6,7,8,9,10};
std::vector<int> result;
std::copy_if(nums.begin(), nums.end(), std::back_inserter(result),
[](int n){ return n % 2 == 0; });
std::transform(result.begin(), result.end(), result.begin(),
[](int n){ return n * n; });
result.resize(3);

五层嵌套,变量传来传去,调试的时候根本不知道数据流向了哪里。

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:#333

1.2 管道操作符:|

Ranges 最大的创新是管道操作符 |,将多个操作串联成链:

1
2
3
4
5
// 原:嵌套调用
auto result = take(transform(filter(nums, pred), func), n);

// 新:管道风格
auto result = nums | filter(pred) | transform(func) | take(n);

读法:数据从左向右”流”过每个变换,像 shell 的管道。

二、常用视图操作

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
38
39
#include <iostream>
#include <vector>
#include <ranges>
#include <algorithm>

namespace rv = std::views;

int main() {
std::vector<int> nums = {1,2,3,4,5,6,7,8,9,10};

// Ranges: 惰性求值,管道组合
auto result = nums
| rv::filter([](int n) { return n % 2 == 0; })
| rv::transform([](int n) { return n * n; })
| rv::take(3);

for (int n : result) {
std::cout << n << " "; // 4 16 36
}
std::cout << "\n";

// views::iota: 无限序列
auto squares = rv::iota(1)
| rv::transform([](int n) { return n * n; })
| rv::take(5);

for (int s : squares) std::cout << s << " "; // 1 4 9 16 25
std::cout << "\n";

// reverse, drop, take
for (int n : nums | rv::reverse | rv::drop(3) | rv::take(4)) {
std::cout << n << " "; // 10 9 8 7
}
std::cout << "\n";

// all + common
auto v = nums | rv::common;
std::sort(v.begin(), v.end());
}

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
2
auto first5 = nums | rv::take(5);   // 前5个
auto rest = nums | rv::drop(3); // 跳过前3个

2.4 reverse:反转

1
auto reversed = nums | rv::reverse;

2.5 iota:无限序列

1
2
3
4
// 生成无限序列:1, 2, 3, 4, 5, ...
auto naturals = rv::iota(1);
// 配合 take 限制数量
auto first100 = rv::iota(1) | rv::take(100);

2.6 all + common:兼容旧代码

1
2
3
// std::views::all 将容器转为 view
auto v = nums | rv::common; // 转为可与旧算法交互的视图
std::sort(v.begin(), v.end());

三、视图组合示意

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

维度传统 STLRanges
组合方式嵌套函数调用管道操作符 `
求值时机即时(容器)惰性(视图)
代码风格命令式声明式
可读性一般✅ 优秀
性能即时分配✅ 按需计算
适用场景简单操作复杂数据流

五、实际应用场景

5.1 数据清洗管道

1
2
3
4
5
auto clean_data = raw_data 
| rv::filter([](auto& r){ return r.valid; })
| rv::transform([](auto& r){ return r.value; })
| rv::drop(100)
| rv::take(50);

5.2 无限序列处理

1
2
3
4
// 生成斐波那契数列
auto fib = rv::iota(0) | rv::transform([](int n){
// 复杂逻辑
});

5.3 字符串处理

1
2
3
4
using namespace std::ranges::views;
auto words = text
| split(' ')
| transform(to_lower);

六、注意事项

  1. Views 不存储数据:迭代器失效后 view 不可用
  2. 不是所有操作都返回 viewto_vector 等终端操作会实例化容器
  3. 编译时间:复杂管道会增加编译时间,但运行时性能通常更好

结论与建议

Ranges 让数据流变成了代码的第一公民| 操作符将算法串联成可读性极高的管道。

什么时候用 Ranges

  • 数据处理链超过 2 个步骤
  • 需要惰性求值避免不必要计算
  • 无限序列操作

什么时候用传统 STL

  • 简单的一次性操作
  • 需要存储结果供后续使用
  • 编译时间敏感的场景

下一步:学习 std::ranges::to(C++23 将容器转换写为 vec | to<std::vector>),以及 Ranges 与协程的结合使用。

记住:Ranges 不是替代 STL 算法,而是增强。你的 std::sortstd::accumulate 依然有效,只是数据流的上游可以使用 Ranges 优雅地组合。


📚 C++20 新特性 系列导航

本文是《C++20 新特性》系列第 7/7 篇。

方向章节
◀ 上一篇(六)Coroutine 协程
📖 全部 7 篇目录(点击展开)
  1. (一)Concepts
  2. (二)requires 表达式
  3. (三)Spaceship Operator
  4. (四)Lambda 加强
  5. (五)consteval 与 constinit
  6. (六)Coroutine 协程
  7. (七)Ranges 库 ← 当前