如何使用 C++20 的 std::ranges 进行高效数据处理

C++20 的 <ranges> 库为我们提供了一套强大的、延迟求值的视图(views)与适配器(adapters),使得数据处理变得既简洁又高效。相比传统的 std::transformstd::copy_if 等函数,ranges 通过链式调用让代码更具可读性,同时避免了不必要的临时对象,提升了性能。下面我们将从基本概念、常用视图、适配器以及实际案例几个方面,深入探讨如何在 C++20 中充分利用 std::ranges

1. 基本概念

  • Range:任何满足 begin()end() 成员函数或全局函数的对象,或者能被 std::begin()std::end() 接收的对象,都可以视为一个 Range。典型的如 `std::vector `、`std::array` 等。
  • View:对一个 Range 的“视图”,不存储数据,只在访问时根据底层 Range 生成元素。典型的如 std::views::filterstd::views::transform 等。
  • Adaptor:对 View 的进一步包装,如 std::views::takestd::views::drop 等。
  • Pipe:通过 | 操作符将多个视图、适配器连接起来,形成一个完整的数据处理链。

2. 常用视图(Views)

视图 功能 示例
std::views::all 直接引用原始 Range auto v = std::views::all(vec);
std::views::iota 生成数值序列 auto seq = std::views::iota(1, 10);
std::views::filter 过滤元素 auto evens = vec | std::views::filter([](int x){ return x%2==0; });
std::views::transform 转换元素 auto squares = vec | std::views::transform([](int x){ return x*x; });
std::views::reverse 反转 auto rev = vec | std::views::reverse;
std::views::take / drop 截取/跳过 auto first3 = vec | std::views::take(3);

3. 常用适配器(Adapters)

适配器主要作用是改变迭代器的特性,常见的有:

  • std::views::common:将不具备 common_range 的 Range 转为具备 common_range,方便在 for 循环中使用。
  • std::views::unique:去重(要求已排序)。
  • std::views::zip(C++23):并行遍历多个 Range。

4. 延迟求值与性能

  • 延迟求值:只有在真正访问元素(如 for 循环、std::copy 等)时,视图才会计算对应的元素。中间结果不会被立即产生,避免了临时容器。
  • 复用:同一个 View 可以多次遍历,且每次遍历都是新的迭代器链,保证了数据的一致性。
  • 内联优化:编译器能够对视图链进行内联展开,将多层调用压缩成单层循环,极大地提升执行速度。

5. 实际案例

5.1 统计整数序列中大于阈值的平方和

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

int main() {
    std::vector <int> data{1, 3, 5, 7, 9, 11};

    int threshold = 5;
    // 统计所有 > threshold 的平方和
    auto sum_of_squares =
        std::accumulate(
            data | std::views::filter([threshold](int x){ return x > threshold; }) |
            std::views::transform([](int x){ return x * x; }),
            0);

    std::cout << "Sum of squares: " << sum_of_squares << '\n';
}

5.2 生成斐波那契序列前 20 项

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

int main() {
    auto fib_seq = std::views::iota(0, 20) | 
                   std::views::transform([](int n){
                       if (n < 2) return n;
                       int a = 0, b = 1, c;
                       for (int i = 2; i <= n; ++i) {
                           c = a + b;
                           a = b; b = c;
                       }
                       return b;
                   });

    for (auto f : fib_seq)
        std::cout << f << ' ';
    std::cout << '\n';
}

5.3 统计字符串中所有单词出现次数

#include <iostream>
#include <sstream>
#include <unordered_map>
#include <ranges>

int main() {
    std::string text = "hello world hello cpp ranges world";
    std::istringstream iss{text};

    std::unordered_map<std::string, int> freq;
    std::for_each(
        std::istream_iterator<std::string>{iss},
        std::istream_iterator<std::string>{},
        [&freq](const std::string& word){ ++freq[word]; });

    for (auto [word, count] : freq)
        std::cout << word << ": " << count << '\n';
}

6. 进阶使用

6.1 组合多个视图

auto processed = vec | 
                 std::views::filter([](int x){ return x % 3 == 0; }) |
                 std::views::transform([](int x){ return x * 2; }) |
                 std::views::reverse |
                 std::views::common;

6.2 自定义视图

通过 std::ranges::view_facade 可以实现自定义的视图。示例:

template<typename Iter>
class even_view : public std::ranges::view_facade<even_view<Iter>> {
    Iter curr_;
public:
    even_view(Iter first, Iter last) : curr_{first} {
        if (curr_ != last && (*curr_ & 1)) ++curr_;
    }

    auto begin() { return curr_; }
    auto end()   { return std::end(*this); }

private:
    friend std::ranges::range_access;
    Iter next(Iter i) {
        // 寻找下一个偶数
        for (++i; i != std::end(*this) && (*i & 1); ++i);
        return i;
    }
};

7. 小结

  • std::ranges 让 C++20 的数据处理更接近函数式编程的风格,代码可读性大幅提升。
  • 通过链式视图与适配器,可在保持延迟求值的同时完成多步数据处理。
  • 对性能要求较高的场景,ranges 通过消除不必要的临时对象和延迟计算,实现了高效的运行时表现。

掌握 std::ranges 后,你可以轻松完成复杂的数据筛选、转换与聚合任务,真正做到“少写代码,多做事”。祝你在 C++20 的世界里玩得愉快!

发表评论