在C++20之前,遍历容器的方式基本是使用迭代器或基于范围的for循环。C++20引入了范围(Range)概念,彻底改变了我们对容器遍历的思维方式。通过范围视图(views)和范围操作(operations),可以在不复制数据的前提下,对序列进行惰性、组合化的变换。本文将从理论与实践两方面,带你快速掌握范围视图的核心要点。
1. 基础概念
1.1 视图(View)
视图是一种对已有序列进行“逻辑”变换的包装器。它不持有自己的数据,而是把变换规则包装成可迭代的对象。常见的视图包括:
std::views::filter:过滤器std::views::transform:变换std::views::reverse:反转std::views::take/std::views::drop:截取/丢弃std::views::zip(C++23):zip合并
1.2 视图的惰性
视图是惰性的,即直到真正遍历时才会计算。这样可以避免不必要的拷贝,甚至在不遍历的情况下不产生任何计算。
1.3 组合与链式调用
视图可以像管道一样组合,例如:
auto nums = std::views::iota(0, 10); // 0-9
auto filtered = nums | std::views::filter([](int n){ return n%2==0; });
auto transformed = filtered | std::views::transform([](int n){ return n*n; });
上述代码构造了一个从0到9,筛选偶数后平方的视图。
2. 典型使用场景
2.1 简洁的过滤与变换
std::vector <int> vec = {1,2,3,4,5,6,7,8,9,10};
auto result = vec
| std::views::filter([](int n){ return n%3==0; })
| std::views::transform([](int n){ return n*n; });
for (int x : result) {
std::cout << x << ' '; // 输出 9 36 81
}
不需要显式循环或中间容器,代码更短、更易维护。
2.2 生成序列
C++20提供了 std::views::iota 来生成连续整数序列。可以与 take、drop、reverse 等组合生成各种序列。
auto seq = std::views::iota(1, 100) | std::views::take(10); // 1~10
auto rev = std::views::iota(1, 10) | std::views::reverse; // 9~1
2.3 组合算法
在标准库中许多算法现在接受 Range,例如 std::ranges::for_each, std::ranges::sort. 与视图配合使用,算法仅作用于需要的子范围。
std::vector <int> v = {5, 1, 4, 2, 3};
auto sorted_part = v | std::views::filter([](int n){ return n < 4; }) | std::views::common;
std::ranges::sort(sorted_part);
3. 性能与注意事项
3.1 惰性执行的好处
- 无拷贝:视图不存储数据,避免不必要的拷贝与分配。
- 按需计算:只在需要时才计算,节省 CPU 资源。
3.2 何时需要 common 视图
视图在默认情况下可能不满足 std::ranges::common_range,这会导致某些算法无法直接使用。可以通过 | std::views::common 强制使其满足:
auto rng = vec | std::views::transform([](int n){ return n*n; }) | std::views::common;
3.3 大数据量与多线程
- 视图天然适合分块处理。可结合
std::ranges::chunk(C++23)或自定义分块来实现并行计算。 - 注意线程安全:视图本身不保证线程安全,若多线程访问同一容器,需要自行加锁或使用并行 STL。
4. 示例:实现一个“延迟过滤器”
下面给出一个自定义视图 lazy_filter,演示如何从头实现一个惰性视图。它与 std::views::filter 功能相同,但演示了内部实现细节。
#include <ranges>
#include <vector>
#include <iostream>
template <std::ranges::input_range R, typename Pred>
class lazy_filter_view : public std::ranges::view_base {
R base_;
Pred pred_;
public:
lazy_filter_view(R r, Pred p) : base_(std::move(r)), pred_(std::move(p)) {}
auto begin() {
auto it = std::ranges::begin(base_);
while (it != std::ranges::end(base_) && !pred_(*it)) {
++it;
}
return it;
}
auto end() {
return std::ranges::end(base_);
}
};
template <typename R, typename Pred>
lazy_filter_view(R&&, Pred) -> lazy_filter_view<std::ranges::remove_cvref_t<R>, Pred>;
int main() {
std::vector <int> nums = {1,2,3,4,5,6};
auto filt = lazy_filter_view(nums, [](int n){ return n%2==0; });
for (int x : filt) {
std::cout << x << ' '; // 输出 2 4 6
}
}
此示例展示了视图如何把遍历逻辑封装成可迭代对象,且对内部实现保持高度可读性。
5. 小结
- 范围视图让容器遍历与变换变得极其简洁且惰性执行,避免无谓拷贝。
- 通过
|组合多个视图,可构造出极具表达力的流水线式算法。 - 与标准算法配合使用,能够直接对子范围做排序、查找等操作。
- 记得在需要时使用
| std::views::common以满足common_range要求。
掌握范围视图后,你的 C++ 代码将更具现代化风格,逻辑更清晰,性能更优。下一步可以尝试深入探索 C++23 的 views::zip 与 views::chunk,以及标准库的 并行 STL,进一步提升代码的表达力与并行性能。祝编码愉快!