在 C++20 之前,处理容器数据往往需要显式循环、拷贝或手写算法。随着标准库的更新,ranges 与 views 的引入让这一切变得更简洁、更高效。本文将通过一组实战例子,展示如何使用 std::ranges 与 std::views 来简化代码、提升可读性,并在保持性能的同时减少错误。
1. Ranges 基础
std::ranges::range 是一种概念(concept),它表示一段可以被迭代的对象。标准容器、原始数组以及自定义类型,只要满足 begin()、end() 或 size() 等成员/非成员函数,即可作为 Range 使用。
#include <vector>
#include <ranges>
std::vector <int> vec = {1, 2, 3, 4, 5};
for (int x : vec | std::ranges::views::filter([](int n){ return n % 2 == 0; })) {
std::cout << x << ' '; // 输出 2 4
}
上例通过管道运算符 | 将视图(view)链接到原始容器,形成了一个“延迟求值”的可迭代对象。所有过滤操作都是按需执行,避免了中间容器的拷贝。
2. 视图(Views)常用类型
| 视图 | 作用 | 代码示例 |
|---|---|---|
std::views::filter |
按条件筛选元素 | view | std::views::filter(p) |
std::views::transform |
对每个元素做变换 | view | std::views::transform(f) |
std::views::reverse |
反转迭代顺序 | view | std::views::reverse |
std::views::take |
取前 N 个元素 | view | std::views::take(n) |
std::views::drop |
跳过前 N 个元素 | view | std::views::drop(n) |
std::views::join |
合并子容器 | view | std::views::join |
2.1 组合使用
#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector<std::vector<int>> data = {{1, 2, 3}, {4, 5}, {6, 7, 8, 9}};
auto flat = data
| std::views::join // 展开成单层视图
| std::views::filter([](int x){ return x % 2 == 0; }) // 只保留偶数
| std::views::transform([](int x){ return x * x; }); // 平方
for (int v : flat) {
std::cout << v << ' '; // 输出 4 16 36 64
}
}
这段代码只用了一行 for 循环,却完成了展开、筛选、变换等多重操作。整个过程都是懒执行,真正的计算在需要时才发生。
3. std::ranges::action 与 std::ranges::subrange
如果你需要把视图的结果写回容器,ranges::actions 提供了便利。
#include <vector>
#include <ranges>
#include <algorithm>
int main() {
std::vector <int> vec = {1, 2, 3, 4, 5};
vec
| std::views::filter([](int n){ return n % 2 == 0; })
| std::ranges::actions::sort(); // 先筛选,再排序
// vec 现在是 {2, 4}
}
std::ranges::actions 作用于范围本身,而不是产生新的范围。你也可以直接使用 std::ranges::subrange 来创建一个自定义范围:
auto sub = std::ranges::subrange(vec.begin() + 1, vec.begin() + 4);
4. 性能注意事项
- 懒求值:所有视图都是懒加载,只有在迭代时才会执行。这样避免了不必要的中间拷贝。
- 复制与引用:
transform的 lambda 默认会捕获值,若你想避免拷贝可以使用std::ref或std::cref。 - 迭代器复杂度:标准视图提供的迭代器均符合
ForwardIterator或更高的概念。reverse视图在BidirectionalIterator上实现,而take、drop视图在RandomAccessIterator上更高效。
5. 与传统算法的对比
下面用一个经典问题:计算一个整数数组中偶数平方之和,分别用传统 std::accumulate 与 Ranges。
// 传统方式
int sum1 = std::accumulate(vec.begin(), vec.end(), 0,
[](int acc, int x){ return acc + ((x % 2 == 0) ? x * x : 0); });
// Ranges 方式
int sum2 = std::accumulate(
vec
| std::views::filter([](int n){ return n % 2 == 0; })
| std::views::transform([](int n){ return n * n; }),
0,
std::plus{}
);
后者代码更清晰,逻辑也更分层。若你使用 C++20 或更高版本,强烈建议在可行的地方使用 Ranges 与 Views。
6. 结语
C++20 的 ranges 与 views 是一次范式的升级,让我们可以像处理“数据流”一样处理容器。通过组合简单的视图,你可以写出既短小又不失可读性的代码,同时保持或提升性能。下一步,你可以尝试将这些概念迁移到更复杂的业务场景,例如大规模日志处理、图数据遍历或并行算法。祝你编码愉快!