题目:C++20 中的 Ranges 与 Views:让你的数据处理更优雅

在 C++20 之前,处理容器数据往往需要显式循环、拷贝或手写算法。随着标准库的更新,rangesviews 的引入让这一切变得更简洁、更高效。本文将通过一组实战例子,展示如何使用 std::rangesstd::views 来简化代码、提升可读性,并在保持性能的同时减少错误。

1. Ranges 基础

std::ranges::range 是一种概念(concept),它表示一段可以被迭代的对象。标准容器、原始数组以及自定义类型,只要满足 begin()end()size() 等成员/非成员函数,即可作为 Range 使用。

#include <vector>
#include <ranges>

std::vector <int> vec = {1, 2, 3, 4, 5};

for (int x : vec | std::ranges::views::filter([](int n){ return n % 2 == 0; })) {
    std::cout << x << ' ';   // 输出 2 4
}

上例通过管道运算符 | 将视图(view)链接到原始容器,形成了一个“延迟求值”的可迭代对象。所有过滤操作都是按需执行,避免了中间容器的拷贝。

2. 视图(Views)常用类型

视图 作用 代码示例
std::views::filter 按条件筛选元素 view | std::views::filter(p)
std::views::transform 对每个元素做变换 view | std::views::transform(f)
std::views::reverse 反转迭代顺序 view | std::views::reverse
std::views::take 取前 N 个元素 view | std::views::take(n)
std::views::drop 跳过前 N 个元素 view | std::views::drop(n)
std::views::join 合并子容器 view | std::views::join

2.1 组合使用

#include <iostream>
#include <vector>
#include <ranges>

int main() {
    std::vector<std::vector<int>> data = {{1, 2, 3}, {4, 5}, {6, 7, 8, 9}};
    auto flat = data 
        | std::views::join                 // 展开成单层视图
        | std::views::filter([](int x){ return x % 2 == 0; }) // 只保留偶数
        | std::views::transform([](int x){ return x * x; });  // 平方

    for (int v : flat) {
        std::cout << v << ' ';   // 输出 4 16 36 64
    }
}

这段代码只用了一行 for 循环,却完成了展开、筛选、变换等多重操作。整个过程都是懒执行,真正的计算在需要时才发生。

3. std::ranges::actionstd::ranges::subrange

如果你需要把视图的结果写回容器,ranges::actions 提供了便利。

#include <vector>
#include <ranges>
#include <algorithm>

int main() {
    std::vector <int> vec = {1, 2, 3, 4, 5};

    vec 
        | std::views::filter([](int n){ return n % 2 == 0; })
        | std::ranges::actions::sort();          // 先筛选,再排序

    // vec 现在是 {2, 4}
}

std::ranges::actions 作用于范围本身,而不是产生新的范围。你也可以直接使用 std::ranges::subrange 来创建一个自定义范围:

auto sub = std::ranges::subrange(vec.begin() + 1, vec.begin() + 4);

4. 性能注意事项

  • 懒求值:所有视图都是懒加载,只有在迭代时才会执行。这样避免了不必要的中间拷贝。
  • 复制与引用transform 的 lambda 默认会捕获值,若你想避免拷贝可以使用 std::refstd::cref
  • 迭代器复杂度:标准视图提供的迭代器均符合 ForwardIterator 或更高的概念。reverse 视图在 BidirectionalIterator 上实现,而 takedrop 视图在 RandomAccessIterator 上更高效。

5. 与传统算法的对比

下面用一个经典问题:计算一个整数数组中偶数平方之和,分别用传统 std::accumulate 与 Ranges。

// 传统方式
int sum1 = std::accumulate(vec.begin(), vec.end(), 0,
    [](int acc, int x){ return acc + ((x % 2 == 0) ? x * x : 0); });

// Ranges 方式
int sum2 = std::accumulate(
    vec 
    | std::views::filter([](int n){ return n % 2 == 0; })
    | std::views::transform([](int n){ return n * n; }),
    0,
    std::plus{}
);

后者代码更清晰,逻辑也更分层。若你使用 C++20 或更高版本,强烈建议在可行的地方使用 Ranges 与 Views。

6. 结语

C++20 的 rangesviews 是一次范式的升级,让我们可以像处理“数据流”一样处理容器。通过组合简单的视图,你可以写出既短小又不失可读性的代码,同时保持或提升性能。下一步,你可以尝试将这些概念迁移到更复杂的业务场景,例如大规模日志处理、图数据遍历或并行算法。祝你编码愉快!


发表评论