利用 C++20 标准库中的 std::ranges:简化数据处理

在 C++20 之后,标准库引入了 std::ranges 子域,为容器、算法以及视图(view)提供了更自然、更安全、更简洁的接口。相较于传统的基于迭代器的算法调用,std::ranges 通过范围(range)来隐藏迭代器细节,让代码更易读、易维护,同时提供了更强的类型安全性。本文将通过几个实际例子来说明 std::ranges 的强大之处,并展示如何在日常项目中快速上手。

1. 传统算法写法与 ranges 对比

#include <vector>
#include <algorithm>
#include <numeric>
#include <iostream>

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

    // 传统写法:使用 std::copy_if
    std::vector <int> even;
    std::copy_if(nums.begin(), nums.end(),
                 std::back_inserter(even),
                 [](int n){ return n % 2 == 0; });

    // 传统写法:求和
    int sum = std::accumulate(nums.begin(), nums.end(), 0);

    std::cout << "Even numbers: ";
    for (int n : even) std::cout << n << ' ';
    std::cout << "\nSum: " << sum << '\n';
}

上述代码在做两个操作:筛选偶数、计算总和。虽然功能完整,但代码中反复出现了迭代器的使用,显得冗长且容易出错。std::ranges 让这些操作可以更简洁地链式调用。

2. ranges 的核心概念

关键词 说明
Range 表示一段可遍历的元素序列,例如 `std::vector
std::arraystd::initializer_list` 等。
View 对原始 Range 的无所有权(non-owning)包装,支持延迟求值(lazy evaluation),如 std::views::filter, std::views::transform 等。
Action 对 View 进行一次性计算的操作,如 std::ranges::for_each, std::ranges::sort 等。

这些概念使得 ranges 能够在表达式层面上构建“管道”,并在需要时才触发执行。

3. 使用 std::ranges 进行筛选和求和

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

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

    // 1. 通过视图筛选偶数
    auto evens = nums | std::views::filter([](int n){ return n % 2 == 0; });

    // 2. 将视图转换为 vector
    std::vector <int> even_vec(evens.begin(), evens.end());

    // 3. 直接在范围上求和
    int sum = std::ranges::accumulate(nums, 0);

    std::cout << "Even numbers: ";
    for (int n : even_vec) std::cout << n << ' ';
    std::cout << "\nSum: " << sum << '\n';
}

关键点

  • 管道符号 (|):将容器与视图链式连接。可视为“从左往右”处理数据。
  • 无迭代器暴露:不再显式写出 begin()/end(),提高代码可读性。
  • 延迟求值evens 并未立刻产生结果,只有在遍历或转换为容器时才执行。

4. 链式多重视图

假设我们想先筛选偶数,再对其平方,最后只保留大于 10 的结果。可以一次性完成:

auto result = nums | 
    std::views::filter([](int n){ return n % 2 == 0; }) |    // 只取偶数
    std::views::transform([](int n){ return n * n; }) |       // 平方
    std::views::filter([](int n){ return n > 10; });           // 只保留 > 10

for (int v : result) std::cout << v << ' ';   // 输出 16 36 64 ...

整个流程以表达式形式写出,极大地提升可维护性。

5. 与容器互操作

虽然 ranges 主要针对容器,但也可以和自定义容器一起使用,只要满足 Range 的概念。下面示例演示自定义链表与 ranges 的结合:

#include <ranges>
#include <iostream>

struct Node {
    int value;
    Node* next = nullptr;
};

class LinkedList {
public:
    using iterator = /* 需要自行实现 */;
    // 只要满足 std::ranges::input_range 就能使用
    // 这里简化省略完整实现
};

int main() {
    LinkedList list;
    // 填充数据...
    // 通过 ranges 进行操作
    for (int v : list | std::views::filter([](int n){ return n % 2 == 0; })) {
        std::cout << v << ' ';
    }
}

实现细节与容器无关,核心是让 LinkedList::iterator 满足输入迭代器的概念。

6. 性能与安全

  • 延迟求值std::views 在遍历时逐个产生元素,避免了临时容器的复制开销。
  • 类型安全ranges 的模板推导更严格,能在编译期捕获错误。例如,错误使用 std::views::filter 与非可调用对象会触发编译错误。
  • 可组合性:视图可以自由组合,且不产生副作用,符合函数式编程的理念。

7. 如何快速上手?

  1. 更新编译器:确保使用支持 C++20 的编译器(GCC 10+, Clang 12+, MSVC 16.10+)。
  2. 头文件:仅需 `#include `。
  3. 从简单案例做起:先在小型程序中尝试 std::views::filterstd::views::transform 等。
  4. 阅读标准:官方文档提供了完整 API 列表与使用示例。
  5. 逐步迁移:将已有的 std::copy_ifstd::transform 替换为对应的 ranges 版本,逐步完善。

8. 结语

std::ranges 是 C++20 重要的语言/库特性之一,借助它可以让代码更短、更易读、更安全。无论是对容器的直接操作,还是对自定义数据结构的使用,ranges 都提供了统一的、现代化的编程方式。建议从小项目开始实践,一旦熟悉后在更大规模代码中逐步推广,必将带来显著的开发效率提升。

发表评论