在 C++20 之前,处理容器的常见模式往往需要显式的循环、迭代器或者 STL 算法,例如 std::for_each, std::transform, std::accumulate 等。随着 C++20 引入的 ranges 库,代码的可读性和可维护性都有了显著提升。本文将通过几个实战例子,展示如何利用 ranges 来简化集合操作,并对其背后的实现机制做简要说明。
1. 预备知识
在使用 ranges 前,需要确保编译器支持 C++20 标准,并在头文件中包含 ranges 的相关头文件:
#include <ranges>
#include <vector>
#include <iostream>
#include <numeric>
std::ranges 主要提供了以下核心概念:
- View:对容器进行惰性、链式变换的“视图”,如
std::views::filter,std::views::transform等。 - Actions:对容器进行立即变换的操作,如
std::ranges::sort,std::ranges::reverse等。 - Range:可迭代对象的抽象,几乎所有标准容器都符合。
2. 过滤与变换
假设我们有一个整数向量,想要得到所有偶数的平方和。传统做法可能是:
std::vector <int> nums{1,2,3,4,5,6};
int sum = 0;
for (int n : nums) {
if (n % 2 == 0) {
sum += n * n;
}
}
std::cout << sum << '\n';
使用 ranges,可以写成:
int sum = std::accumulate(
nums | std::views::filter([](int n){ return n % 2 == 0; }) |
std::views::transform([](int n){ return n * n; }),
0, std::plus{}
);
std::cout << sum << '\n';
这里的关键点:
nums | std::views::filter(...):返回一个惰性过滤视图,仅在需要时才检查元素。| std::views::transform(...):链式变换,将每个偶数映射为其平方。std::accumulate:对视图中的元素进行累加。
这种方式的优点是:
- 代码更加声明式,描述的是“做什么”,而非“怎么做”。
- 视图是惰性的,避免了中间容器的创建,提高性能。
3. 组合视图与排序
有时我们需要先过滤、再排序,再取前几个结果。下面演示如何把这些步骤整合:
auto top_three = nums
| std::views::filter([](int n){ return n > 3; })
| std::views::transform([](int n){ return std::pair{n, n*n}; })
| std::views::take(3)
| std::views::reverse; // 取最大的 3 个
for (auto [val, sq] : top_three) {
std::cout << val << '^2 = ' << sq << '\n';
}
在这里:
std::views::take(3)直接限制视图长度,无需创建临时容器。std::views::reverse在已取完前三个后逆序,得到降序排列。
4. 修改容器的动作
如果想对容器本身做变换(如排序),可以使用 ranges 的 action:
auto vec = std::vector <int>{3, 1, 4, 1, 5, 9};
std::ranges::sort(vec); // 原地排序
std::ranges::reverse(vec); // 原地反转
这些动作与传统 std::sort 的区别在于语义更清晰,同时可以直接作用于任何符合 range 概念的容器。
5. 自定义 View
有时标准视图不够用,你可以自定义一个简单的视图。例如,一个“偶数索引”视图:
template<std::ranges::input_range R>
requires std::ranges::view <R>
auto even_index_view(R&& r)
{
return std::views::transform(std::forward <R>(r),
[idx = 0, i = 0](auto&& x) mutable {
if (i % 2 == 0) {
return x;
}
++idx;
return std::nullopt; // 过滤掉奇数索引
})
| std::views::filter([](auto&& x){ return static_cast <bool>(x); })
| std::views::transform([](auto&& x){ return *x; });
}
虽然略显冗长,但展示了 ranges 的灵活性。利用 views::transform 的闭包,你可以在一次遍历中完成多种复杂逻辑。
6. 性能考虑
- 惰性 vs 立即:视图是惰性的,适用于需要链式操作而不想产生中间容器的场景。若操作非常简单且数据量大,惰性可能会产生额外的迭代器包装成本,影响性能。此时可以考虑使用 action 或者直接 STL 算法。
- 缓存视图:若同一个视图会多次使用,建议将其存入
auto变量,避免每次都重新创建。
7. 小结
C++20 的 ranges 库为集合操作提供了更自然、更高层次的表达方式。通过视图和动作的组合,代码可读性显著提升,且在大多数场景下性能不亚于手写循环。建议在现代 C++ 项目中逐步引入 ranges,尤其是需要频繁对容器做过滤、变换、聚合等操作时。