在 C++20 中,ranges 和 view 彻底改变了我们处理序列的方式。相比传统的 STL 容器与算法,ranges 通过惰性求值、链式组合与类型擦除,让代码既简洁又高效。本文将从基础概念入手,展示如何在实际项目中充分利用 ranges 与 view,提升代码可读性与性能。
1. 基础概念
1.1 范围(Range)
一个范围是一对迭代器(begin 与 end)所定义的序列。C++20 引入了 std::ranges::range 范围概念,用来标识所有满足 begin() 与 end() 接口的类型。
#include <ranges>
#include <vector>
std::vector <int> vec{1, 2, 3, 4, 5};
static_assert(std::ranges::range<std::vector<int>>); // true
1.2 View
View 是对范围的“视图”,它并不存储数据,而是通过惰性计算得到结果。常见的 View 有 std::ranges::filter_view、std::ranges::transform_view、std::ranges::reverse_view 等。
auto even_view = vec | std::views::filter([](int x){ return x % 2 == 0; });
2. 常用 View 示例
2.1 过滤(filter)
只保留满足条件的元素。
for (int x : vec | std::views::filter([](int n){ return n > 2; }))
std::cout << x << ' '; // 3 4 5
2.2 转换(transform)
对每个元素做一次映射。
for (int x : vec | std::views::transform([](int n){ return n * n; }))
std::cout << x << ' '; // 1 4 9 16 25
2.3 组合(concatenate)
将多个 View 合并为一个。
std::vector <int> vec2{6, 7, 8};
auto combined = vec | std::views::concat(vec2);
for (int x : combined)
std::cout << x << ' '; // 1 2 3 4 5 6 7 8
2.4 递归(views::iota 与 views::take)
生成无穷序列并截断。
auto first10 = std::views::iota(0) | std::views::take(10);
for (int x : first10)
std::cout << x << ' '; // 0 1 2 3 4 5 6 7 8 9
3. 性能优势
3.1 惰性求值
View 采用惰性求值策略,只有在真正访问元素时才执行计算,避免了不必要的拷贝或中间结果。
auto chain = vec | std::views::filter([](int n){ return n % 2 == 0; })
| std::views::transform([](int n){ return n * 2; });
上面链式调用仅在 for 循环访问元素时执行一次过滤再一次映射。
3.2 编译期优化
与传统 std::transform 等算法相比,ranges 的 View 对象是轻量级的,编译器可以更容易地消除临时对象并进行函数内联。
3.3 并行支持
C++20 ranges 与 view 与并行执行器(如 std::execution::par)兼容,可直接使用 std::ranges::for_each 等算法实现并行处理。
std::ranges::for_each(vec | std::views::filter([](int n){ return n > 3; }),
std::execution::par,
[](int n){ std::cout << n << ' '; });
4. 与传统 STL 的对比
| 任务 | STL 写法 | ranges 写法 |
|---|---|---|
| 过滤偶数 | std::copy_if |
std::views::filter |
| 取平方 | std::transform |
std::views::transform |
| 链式操作 | 多次临时容器 | 单一惰性链 |
| 并行 | std::for_each + execution::par |
std::ranges::for_each + execution::par |
4.1 代码可读性提升
使用 ranges,代码更像自然语言,读者可以直观看到“过滤 -> 转换 -> 迭代”的流程,而不必关心中间容器。
4.2 内存占用减少
由于 View 不会产生中间容器,内存占用大幅下降,尤其适用于大数据量处理。
5. 真实案例
假设我们有一个包含日志条目的 `std::vector
`,想要提取最近一小时内所有错误级别的日志,并打印其简要摘要。 “`cpp struct LogEntry { std::chrono::system_clock::time_point timestamp; std::string level; // “INFO”, “WARN”, “ERROR” std::string message; }; std::vector logs = load_logs(); auto now = std::chrono::system_clock::now(); auto recent_errors = logs | std::views::filter([now](const LogEntry& e){ return e.level == “ERROR” && e.timestamp >= now – std::chrono::hours{1}; }) | std::views::transform([](const LogEntry& e){ return e.message; }); for (const auto& msg : recent_errors) std::cout