**标题**:如何在 C++20 中使用 std::ranges 进行链式容器操作?

文章内容

C++20 引入了 std::ranges 库,彻底改变了我们对容器操作的思维方式。借助 viewsactions 以及 ranges 工具,我们可以用更简洁、声明式的代码实现往往在传统算法中需要手动写循环、临时容器等繁琐步骤的功能。下面通过一个完整的示例,展示如何使用 std::ranges 对容器进行链式过滤、映射、排序、去重等操作。

#include <iostream>
#include <vector>
#include <string>
#include <ranges>
#include <algorithm>

int main()
{
    // 原始数据:一个包含整数的 vector
    std::vector <int> nums{3, 1, 4, 1, 5, 9, 2, 6, 5, 3};

    // 目标:
    // 1. 只保留偶数
    // 2. 每个偶数乘以 2
    // 3. 按升序排序
    // 4. 去重
    // 5. 转换为字符串并拼接成单个字符串

    // 使用 ranges 的链式操作
    auto result = nums 
        | std::views::filter([](int n){ return n % 2 == 0; })        // 只保留偶数
        | std::views::transform([](int n){ return n * 2; })          // 乘以 2
        | std::views::common();                                      // 转为 common_view(使得可多次遍历)
        // 对 common_view 进行排序和去重
        ;

    // 需要一个临时容器来执行排序和去重(因为 views 本身是惰性的)
    std::vector <int> temp(result.begin(), result.end());
    std::ranges::sort(temp);                         // 排序
    auto last = std::ranges::unique(temp);          // 去重
    temp.erase(last, temp.end());

    // 转换为字符串
    std::string joined;
    for (auto n : temp) {
        joined += std::to_string(n);
        joined += ",";
    }
    if (!joined.empty()) joined.pop_back(); // 去掉最后一个逗号

    std::cout << "处理后的结果: " << joined << '\n';
}

代码逐步解析

  1. 过滤
    std::views::filter 接受一个谓词,用于筛选符合条件的元素。这里我们保留 n % 2 == 0 的偶数。

  2. 映射
    std::views::transform 对每个元素应用一个函数。我们将每个偶数乘以 2。

  3. common_view
    views::common 把惰性视图转换为一个可多次遍历的 common_view。这在后续需要多次遍历(例如排序)时很有用。common_view 还会把底层容器的差异化视图统一成一个通用的 iterator

  4. 排序
    std::ranges::sort 需要可随机访问迭代器,因此我们先把视图转换成 `std::vector

    `,然后调用 `sort`。
  5. 去重
    std::ranges::unique 在已排序的容器上进行去重,返回去重后元素的最后一个位置的迭代器。随后用 erase 删除多余部分。

  6. 转换为字符串
    遍历最终结果,使用 std::to_string 转成字符串并拼接。这里可以进一步使用 std::ranges::join 结合 std::views::transform 实现更函数式的拼接(C++23 将进一步完善这一点)。

高级技巧

  • 自定义视图
    如果你频繁需要某一套操作(如上述四步),可以将它封装成一个自定义视图或一个辅助函数,进一步提升代码可读性。

  • 管道操作
    C++23 将引入 std::ranges::pipeline,让你可以像 Python 的管道一样链式写操作,语法会更加直观。

  • 组合视图
    你可以通过 views::filterviews::transformviews::unique 等直接在一个视图链中完成,避免临时容器,提升性能。例如:

    auto final_view = nums 
        | std::views::filter([](int n){ return n % 2 == 0; })
        | std::views::transform([](int n){ return n * 2; })
        | std::views::unique(); // unique 在已排序的前提下有效

    但需要注意,unique 要求底层迭代器是随机访问,或者你先用 views::commonviews::unique

性能与资源

  • 惰性求值
    std::views 默认是惰性的,意味着它们只在真正需要时才会产生结果。若链中包含耗时操作,尽量将其放在最前面,或使用 views::common 将结果缓冲。

  • 内存占用
    由于视图不存储数据,它们对内存占用友好;但若需要排序、去重等需要随机访问的操作,必须先把结果缓冲到容器中。

  • 编译器优化
    现代编译器(如 GCC 13、Clang 15、MSVC 19.35+)对 ranges 的内联与循环合并做了极大优化,实际速度往往和手写循环相当,甚至更快。

小结

C++20 的 std::ranges 让容器操作从繁琐的迭代器手写,转变为声明式、可组合的表达式。通过上述示例,你可以快速搭建起自己的数据处理流水线,既保持代码简洁,又不失高性能。随着未来标准的演进(C++23、C++26 等),ranges 将继续完善,成为 C++ 生态中不可或缺的工具之一。

发表评论