如何在C++20中使用 std::ranges 实现链式过滤与变换?

在 C++20 引入的 库后,操作序列变得更像函数式编程。通过 std::views::filterstd::views::transform 等视图(view),可以在不产生临时容器的情况下,链式地对数据进行过滤、变换、排序等操作。下面我们用一个完整的例子演示如何在 C++20 中使用 std::ranges 来完成“从整数序列中过滤出偶数,再将其平方,最后求和”的任务,并比较传统方式与新方式的代码可读性与性能差异。

1. 基础示例:传统方式

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

int main() {
    std::vector <int> nums{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    std::vector <int> evens;
    for (int n : nums)
        if (n % 2 == 0) evens.push_back(n);

    std::vector <int> squares;
    for (int e : evens)
        squares.push_back(e * e);

    int sum = std::accumulate(squares.begin(), squares.end(), 0);
    std::cout << "sum = " << sum << '\n';
}

该代码在每一步都显式创建了中间容器,阅读时需要逐行跟踪 evenssquares 的生成过程。

2. 使用 std::ranges 的链式写法

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

int main() {
    std::vector <int> nums{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    int sum = std::ranges::accumulate(
        nums 
        | std::views::filter([](int n){ return n % 2 == 0; })
        | std::views::transform([](int n){ return n * n; }),
        0);

    std::cout << "sum = " << sum << '\n';
}

关键点说明

  • | 运算符用于将视图链式组合,读取时从左到右,像流水线一样。
  • std::views::filter 接受一个可调用对象,返回一个可迭代对象,仅包含满足条件的元素。
  • std::views::transform 对每个元素应用变换函数,返回变换后的值。
  • std::ranges::accumulatestd::accumulate 类似,但接受任何可迭代对象。

3. 性能对比

使用视图时,C++20 的实现通常会采用 惰性求值(lazy evaluation):在调用 accumulate 时,整个视图链会按需逐元素处理,避免了中间容器的构造。实际上编译器会生成类似以下的伪代码:

for each element e in nums:
    if e % 2 == 0:
        sum += e * e;

这与手写的循环完全等价,且编译器可以进一步优化,例如消除多余的函数调用、指令重排等。

如果使用传统方式,虽然可以通过 reservein-place 操作来减少临时容器,但代码仍然显得冗长且不够直观。通过 std::ranges,我们将关注点从“如何存储中间结果”转移到“如何表达业务逻辑”,代码更易维护。

4. 进阶使用:自定义视图

除了标准视图,还可以自己实现一个简单的视图来完成特定需求。例如,下面的 std::views::inplace_transform 直接在原序列上进行变换,而不产生新容器:

namespace std::views {
    template<class F>
    struct inplace_transform_view {
        std::vector <int>& data;
        F f;
        auto begin() { return data.begin(); }
        auto end()   { return data.end(); }
        struct iterator {
            std::vector <int>::iterator it;
            F* f;
            iterator(std::vector <int>::iterator it, F* f) : it(it), f(f) {}
            int& operator*() const { return *it; }
            iterator& operator++() { ++it; return *this; }
            bool operator!=(const iterator& other) const { return it != other.it; }
        };
        iterator begin_it() { return {begin(), &f}; }
        iterator end_it()   { return {end(), &f}; }
    };

    template<class F>
    auto inplace_transform(std::vector <int>& v, F f) {
        return inplace_transform_view <F>{v, f};
    }
}

使用方式:

std::vector <int> nums{1,2,3,4,5,6};
for (auto& x : std::views::inplace_transform(nums, [](int& n){ n *= n; })) {}
// nums 现在已经是平方后的序列

5. 小结

  • std::ranges 让 C++20 的容器与算法更像函数式语言的管道。
  • 通过 views::filterviews::transform 等组合,代码可读性大幅提升,且性能不落后于手写循环。
  • 视图采用惰性求值,避免不必要的中间容器。
  • 自定义视图也可根据需求扩展更细粒度的功能。

如果你正在维护一段需要多次对数据进行变换的 C++ 代码,建议先把逻辑拆解成一组视图,尝试用 std::ranges 重写。你会发现,代码不仅更简洁,也更容易推理和测试。祝编码愉快!

发表评论