你是否还在为嵌套的
for循环和繁琐的 STL 算法调用而烦恼?是否觉得数据处理代码的意图不够直观?C++20 Ranges 库的到来,将彻底改变你操作数据序列的方式。
系列文章索引
- 第 1 篇: 《告别"祖传C++":开启你的现代C++之旅》
- 第 2 篇: 《现代C++的基石:你不得不知的C++11/14/17核心特性》
- 第 3 篇: 《C++20 Concepts:让模板错误信息不再"天书"》
- 第 4 篇: 《C++20 Ranges:告别手写循环,像 SQL 一样操作数据》
- 第 5 篇: 《C++20 协程初探:用同步思维写异步代码》
- 第 6 篇: 《C++20 Modules:终结"头文件地狱"的曙光》
- 第 7 篇: 《尝鲜C++23:std::mdspan、std::expected与更多实用利器》
- 第 8 篇: 《实战演练:用现代 C++ 重构一个"老项目"》
- 第 9 篇: 《现代 C++ 最佳实践清单:编写更安全、更高效的代码》
0. 前言:数据处理的新范式
在 C++20 之前,我们处理一个数据序列(如 std::vector)通常有两种方式:
- 手写
for循环:命令式风格,步骤清晰,但容易将业务逻辑与循环控制混在一起,代码冗长且难以复用。 - STL 算法 (
std::sort,std::transform等):函数式风格,但常常需要配合迭代器(begin(),end()),并且当需要组合多个操作时,会产生许多临时的中间容器,既影响性能又让代码变得支离破碎。
C++20 引入的 Ranges(范围)库 ,提供了一种全新的声明式数据处理范式。它允许你像写 SQL 查询或 Linux 管道一样,用一种清晰、可组合且高效的方式来表达对数据的操作。
1. "之前"的世界:命令式循环的痛苦
让我们来看一个经典的需求:从一个整数集合中,筛选出所有偶数,将它们平方,然后取出前 5 个结果。
传统 for 循环实现
cpp
#include <vector>
#include <iostream>
std::vector<int> process_numbers(const std::vector<int>& input) {
std::vector<int> result;
for (int num : input) {
if (num % 2 == 0) {
int squared = num * num;
if (result.size() < 5) {
result.push_back(squared);
} else {
break; // 手动控制循环退出
}
}
}
return result;
}
痛点分析:
- 逻辑混杂:筛选、转换、限制数量的逻辑都挤在一个循环里。
- 可读性差:需要阅读整个循环才能理解最终结果是如何得出的。
- 手动控制 :需要手动管理
result容器和循环的退出条件。
2. Ranges 的核心思想:视图与管道
Ranges 库的核心是两个概念:视图 和 管道。
- 视图:视图是一个"懒加载"的、非拥有的范围。它不持有数据,只是对现有数据的一种"投影"或"滤镜"。对视图的操作不会立即执行,也不会产生新的容器,因此几乎没有性能开销。
- 管道 :使用管道操作符
|,你可以将多个视图串联起来,形成一个数据处理管道。数据会像水流一样,依次通过管道中的每一个过滤器。
这种组合方式,让代码的读法和写法完全一致,极具表现力。
3. "之后"的世界:Ranges 的优雅
现在,让我们用 Ranges 来重写上面的需求。
cpp
#include <vector>
#include <ranges> // Ranges 库的头文件
#include <algorithm> // for std::ranges::to (C++23) 或其他动作
#include <iostream>
// 为了在 C++20 中使用 to_vector,需要一个简单的辅助函数
// (C++23 将直接提供 std::ranges::to)
template<std::ranges::range R>
auto to_vector(R&& r) {
std::vector<std::ranges::range_value_t<R>> v;
for (auto&& e : r) {
v.push_back(std::forward<decltype(e)>(e));
}
return v;
}
std::vector<int> process_numbers_ranges(const std::vector<int>& input) {
auto pipeline = input
| std::views::filter([](int n) { return n % 2 == 0; }) // 筛选偶数
| std::views::transform([](int n) { return n * n; }) // 平方
| std::views::take(5); // 取前 5 个
// 管道是懒加载的,只有在这里(一个"动作")才会真正执行计算
return to_vector(pipeline);
}
优点分析:
- 声明式:代码清晰地"声明"了我们想要做什么:筛选、转换、取 5 个。它没有描述"如何"做。
- 可读性极高:代码从左到右读,就像一句自然语言。
- 可组合性强:每个视图都是独立的,可以像乐高积木一样随意组合。
- 性能优异 :由于视图的懒加载特性,整个管道操作不会产生任何中间
std::vector,数据是逐个元素流过整个管道的,效率极高。
4. 常用视图一览
Ranges 库提供了大量实用的视图,以下是一些最常用的:
std::views::filter(pred):只保留满足谓词pred的元素。std::views::transform(func):对每个元素应用函数func。std::views::take(n)/std::views::drop(n):取前n个元素 / 跳过前n个元素。std::views::keys/std::views::values:对于一个键值对的范围(如std::map),分别提取所有的键或所有的值。
cpp
#include <map>
std::map<std::string, int> scores = {{"Alice", 90}, {"Bob", 85}, {"Charlie", 95}};
// 提取所有学生姓名
auto names = scores | std::views::keys;
// 提取所有分数
auto values = scores | std::views::values;
std::views::split(delimiter):按分隔符delimiter分割一个范围,常用于字符串处理。
cpp
std::string s = "hello,world,cpp";
auto parts = s | std::views::split(',');
// parts 的结果是三个子范围的视图:['h','e','l','l','o'], ['w','o','r','l','d'], ['c','p','p']
5. 总结与展望
Ranges 库通过引入声明式的管道操作,彻底革新了 C++ 中数据处理的范式。它让我们能够告别繁琐的 for 循环和低效的中间容器,写出既简洁又高性能的代码。
它的核心优势在于:
- 可读性:代码即意图,一目了然。
- 可组合性:像搭积木一样构建复杂的数据处理逻辑。
- 高性能:懒加载视图避免了不必要的计算和内存分配。
Ranges 是 C++20 中最激动人心的特性之一,它让 C++ 在数据处理领域向现代函数式编程语言看齐。
在你的下一个数据处理任务中,放弃 for 循环,尝试用 Ranges 管道来构建它吧!
你会发现,代码的逻辑从未如此清晰。在下一篇文章中,我们将探索 C++20 的另一大杀器:协程,看看它如何用同步的思维来处理异步任务。