C++20 引入了 Range 库,它将容器、视图(view)和算法的概念统一为一个“数据流”模型。相比传统的基于迭代器的写法,Range 语义更为简洁、表达力更强,并且可以天然与并行算法(Parallel Algorithms)配合,进一步提升性能。本文将从 Range 的基础概念、视图的使用、以及如何结合并行算法实现高效排序等方面进行详细剖析。
1. Range 的核心概念
Range 基本上是两个概念的组合:
| 词汇 | 说明 |
|---|---|
View |
一个“只读”的、懒加载的数据序列,类似于惰性生成器。 |
Adaptor |
对已有范围进行变换(如 transform、filter、take 等)。 |
Algorithm |
对视图进行处理的函数,如 for_each、sort、unique 等。 |
在 C++20 之前,算法需要迭代器作为输入,而 Range 则直接使用容器或视图。
#include <ranges>
#include <vector>
#include <iostream>
std::vector <int> vec = {5, 2, 9, 1, 5, 6};
for (auto n : vec | std::views::filter([](int x){ return x % 2 == 0; })) {
std::cout << n << ' ';
}
上述代码过滤出偶数并打印,语义更直观、可读性更高。
2. 常用的 View 与 Adaptor
| 视图 | 作用 | 示例 |
|---|---|---|
std::views::filter |
过滤 | v | std::views::filter([](int x){ return x > 0; }) |
std::views::transform |
变换 | v | std::views::transform([](int x){ return x * 2; }) |
std::views::take |
截取 | v | std::views::take(3) |
std::views::reverse |
反转 | v | std::views::reverse |
std::views::join |
链接 | std::vector<std::vector<int>> vv = {{1,2},{3,4}};auto joined = vv | std::views::join; |
这些视图都是惰性求值的——真正遍历时才会触发计算,极大减少了中间结果的存储开销。
3. 并行算法的使用
C++17 开始提供 std::execution 命名空间,支持并行执行算法。C++20 将其与 Range 结合,形成:
#include <execution>
#include <numeric>
#include <vector>
#include <iostream>
std::vector <int> data(1'000'000);
std::iota(data.begin(), data.end(), 1); // 生成 1..1e6
auto sum = std::reduce(std::execution::par, data.begin(), data.end());
std::cout << "Sum = " << sum << '\n';
若想对一个视图使用并行算法,需要先将视图转换为 std::ranges::subrange 或使用 std::ranges::views::all:
auto filtered = data | std::views::filter([](int x){ return x % 2 == 0; });
auto avg = std::reduce(std::execution::par, filtered.begin(), filtered.end()) / static_cast <double>(filtered.size());
需要注意的是,并行算法要求可随机访问的迭代器(RandomAccessIterator),因此 std::views::filter 等视图本身不满足,但可以通过 std::views::all 将其转为符合要求的范围。
4. 结合 Range 与并行排序的完整示例
下面给出一个完整示例:对一个大型随机整数向量进行去重、排序,并行计算最大值与最小值。
#include <algorithm>
#include <execution>
#include <iostream>
#include <numeric>
#include <random>
#include <ranges>
#include <vector>
int main() {
constexpr std::size_t N = 10'000'000;
std::vector <int> data(N);
std::mt19937 rng{std::random_device{}()};
std::uniform_int_distribution <int> dist(1, 1'000'000);
std::generate(data.begin(), data.end(), [&](){ return dist(rng); });
// 1. 去重:使用 sorted + unique
std::sort(std::execution::par, data.begin(), data.end());
auto last = std::unique(data.begin(), data.end());
data.erase(last, data.end());
// 2. 计算最大值与最小值(并行)
auto max_val = *std::max_element(std::execution::par, data.begin(), data.end());
auto min_val = *std::min_element(std::execution::par, data.begin(), data.end());
std::cout << "unique count: " << data.size() << '\n';
std::cout << "min: " << min_val << ", max: " << max_val << '\n';
// 3. 计算平均值:使用 Range + Parallel Reduce
auto avg = std::reduce(std::execution::par, data.begin(), data.end()) / static_cast <double>(data.size());
std::cout << "average: " << avg << '\n';
}
说明:
std::sort与std::unique直接接受std::execution::par,利用多核并行。max_element、min_element也支持并行。std::reduce并行求和,最后除以长度得到平均值。
该程序在多核机器上可明显加速,尤其是数据量巨大时效果更为突出。
5. 性能对比与调优建议
| 方案 | 代码量 | 可读性 | 性能(典型多核) |
|---|---|---|---|
| 传统迭代器 + 手写循环 | 较多 | 较差 | 较好 |
| Range + 串行算法 | 较少 | 好 | 中等 |
| Range + 并行算法 | 较少 | 好 | 优秀 |
调优建议
- 避免过度惰性:惰性视图在每次遍历时都会产生函数调用,若视图链很长,可能导致性能下降。可以在需要时将视图转换为
std::vector或std::array,一次性消化。 - 选择合适的执行策略:
std::execution::par对于大数据量并行效果好,但对小数据量或 I/O 密集型任务无效,甚至有负担。可根据数据大小动态切换策略。 - 分块并行:对极大容器,可将其拆分为子块,每块使用 Range + 并行处理,然后合并结果。C++23 的
std::execution::par_unseq进一步支持矢量化并行。
6. 小结
C++20 的 Range 彻底改变了我们处理序列的方式:从可迭代的概念演进为数据流,视图(view)提供了高效的惰性变换,算法则可以直接作用于视图。结合 C++17 引入的并行算法,Range 与并行算法的组合让 C++ 在数据处理、数值计算等领域的性能得到质的飞跃。
实战提示:在实际项目中,先从
std::views::filter+std::views::transform开始书写可读性高的代码,随后针对性能瓶颈引入std::execution::par或par_unseq,并通过std::ranges::subrange进行范围转换,最终实现“高效且易维护”的数据处理流水线。