在 C++20 中,标准库引入了 Ranges 子系统,彻底改变了我们处理序列数据的方式。相比传统的迭代器 + 算法模式,Ranges 通过范围(range)和范围适配器(range adaptor)让代码更简洁、易读。本文将带你从基础概念讲起,展示如何使用 Ranges 进行常见的数据处理任务,并分享一些实用的技巧。
1. 基础概念回顾
1.1 范围(Range)
范围是一种可以产生一系列值的对象,它至少要满足以下两个要求:
begin()返回一个可前向/后向/随机访问的迭代器;end()返回一个指向序列末尾的迭代器。
在 C++20 中,任何支持 begin()/end() 的对象都可以被视为范围,包括 STL 容器、C-style 数组、std::initializer_list 等。
1.2 范围适配器(Range Adaptor)
范围适配器是一种函数对象,用来对已有范围进行“变换”或“过滤”。它的工作方式类似于算法,但更像是管道式操作:适配器返回一个新的范围,随后可以再继续链式调用。
常见的适配器包括:
std::views::filter:按条件过滤元素。std::views::transform:对元素做映射。std::views::reverse:反转顺序。std::views::take/std::views::drop:截取/跳过前 n 个元素。std::views::unique:去重(需要排序后使用)。
2. 基本使用示例
2.1 过滤奇数
#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector <int> numbers{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto odds = numbers | std::views::filter([](int n){ return n % 2 == 1; });
for (int n : odds) {
std::cout << n << ' ';
}
// 输出: 1 3 5 7 9
}
2.2 平方并排序
#include <iostream>
#include <vector>
#include <ranges>
#include <algorithm>
int main() {
std::vector <int> nums{3, 1, 4, 1, 5, 9, 2, 6};
auto squares = nums
| std::views::transform([](int n){ return n * n; })
| std::views::common; // 转为可随机访问范围
std::sort(squares.begin(), squares.end());
for (int n : squares) std::cout << n << ' ';
// 输出: 1 1 4 9 16 25 36 81
}
2.3 取前 5 个偶数的平方和
#include <iostream>
#include <vector>
#include <ranges>
#include <numeric>
int main() {
std::vector <int> v{3, 2, 4, 7, 8, 1, 6, 5, 10};
int sum = v
| std::views::filter([](int n){ return n % 2 == 0; })
| std::views::transform([](int n){ return n * n; })
| std::views::take(5)
| std::ranges::accumulate(0, std::plus<>());
std::cout << "sum = " << sum << '\n';
// 输出: sum = 244 (2^2 + 4^2 + 8^2 + 6^2 + 10^2)
}
3. 进阶技巧
3.1 views::common 与视图类型
大部分视图是惰性求值(lazy)的,迭代器仅在需要时才产生。views::common 将视图包装为常规的容器类型,提供 size()、operator[] 等操作,适合需要多次遍历或随机访问的场景。
auto rng = std::views::iota(0, 1000) | std::views::common;
std::cout << rng[500] << '\n'; // 500
3.2 自定义视图
如果标准视图无法满足需求,可以自定义一个视图。最简洁的方式是使用 std::ranges::view_interface。示例:一个生成 Fibonacci 数列的视图。
#include <ranges>
struct fibonacci_view : std::ranges::view_interface <fibonacci_view> {
struct iterator {
std::size_t index{};
std::size_t a{0}, b{1};
auto& operator++() { std::swap(a, b); b += a; ++index; return *this; }
auto operator*() const { return a; }
bool operator==(iterator const&) const { return index == std::numeric_limits<std::size_t>::max(); }
};
auto begin() const { return iterator{}; }
auto end() const { return iterator{std::numeric_limits<std::size_t>::max()}; }
};
int main() {
auto fibs = fibonacci_view{} | std::views::take(10);
for (auto n : fibs) std::cout << n << ' ';
// 输出: 0 1 1 2 3 5 8 13 21 34
}
3.3 视图与 std::move 的配合
在链式调用中,如果你想在中间消耗一次范围(例如求和),可以使用 std::ranges::accumulate 或 std::ranges::for_each,这些函数会从左到右一次遍历,避免产生临时容器。
auto sum = std::views::iota(1, 1000000)
| std::views::transform([](int n){ return n * 2; })
| std::ranges::accumulate(0, std::plus<>());
4. 与旧版算法对比
| 任务 | 传统写法 | Ranges 写法 |
|---|---|---|
| 过滤偶数并平方 | std::transform + std::copy_if |
| std::views::filter | std::views::transform |
| 排序 | std::sort(v.begin(), v.end()) |
std::ranges::sort(v) |
| 取前 n 个 | `std::vector | |
res(v.begin(), v.begin()+n)| |
std::views::take(n)` |
Ranges 的优势显而易见:语法简洁、表达意图直观、惰性求值减少不必要的拷贝。
5. 常见陷阱
-
视图只能一次遍历
惰性视图是一次性使用的。若需多次遍历,先转为std::vector或使用views::common。 -
不支持所有容器
仅支持满足begin()/end()的范围。若想对自定义容器使用,需实现这些成员。 -
std::views::unique要求已排序
若对未排序的范围使用,会得到意外结果。先使用std::ranges::sort再去重。
6. 结语
C++20 的 Ranges 子系统为处理序列数据提供了强大且优雅的工具。通过学习并灵活使用视图与适配器,你可以让代码更加声明式、可读性更高,且常常能减少内存占用与运行时间。建议从日常项目中挑选合适的场景,逐步将传统算法迁移到 Ranges,感受其带来的改变。祝你编码愉快!