如何使用 C++20 的 ranges 来简化集合操作

在 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:对视图中的元素进行累加。

这种方式的优点是:

  1. 代码更加声明式,描述的是“做什么”,而非“怎么做”。
  2. 视图是惰性的,避免了中间容器的创建,提高性能。

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,尤其是需要频繁对容器做过滤、变换、聚合等操作时。


发表评论