C++20中的范围(Range)视图:让容器遍历更灵活

在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 来生成连续整数序列。可以与 takedropreverse 等组合生成各种序列。

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++23views::zipviews::chunk,以及标准库的 并行 STL,进一步提升代码的表达力与并行性能。祝编码愉快!

发表评论