如何在 C++20 中使用 std::span 与 std::ranges 来简化数组处理

在 C++20 之前,处理数组和容器的“视图”通常需要自己手动实现或依赖第三方库,例如 Boost。C++20 引入了 std::spanstd::ranges,这两者结合起来可以极大地简化数组、向量和其他容器的子集处理。本文将从基本概念入手,演示如何使用 std::spanstd::ranges 对数组进行切片、过滤和变换,帮助你在项目中更高效、更安全地处理数据。

1. 何为 std::span

std::span 是一个非拥有的“视图”类型,表示一段连续的内存块。它不管理内存生命周期,只提供对已有数据的只读或可写访问。典型用法如下:

#include <span>
#include <vector>
#include <iostream>

void print_span(std::span <int> s) {
    for (auto v : s) std::cout << v << ' ';
    std::cout << '\n';
}

int main() {
    int arr[] = {1,2,3,4,5,6};
    std::span <int> whole(arr);   // 覆盖整个数组
    std::span <int> half(arr, 3); // 覆盖前3个元素

    print_span(whole); // 输出: 1 2 3 4 5 6
    print_span(half);  // 输出: 1 2 3
}
  • `std::span ` 与 `std::span` 区分只读/可写。
  • 通过 std::as_bytes 可以将任意类型转换为字节视图,常用于序列化。

2. std::ranges 的基本概念

std::ranges 引入了一套视图(views)和算法(algorithms),使得链式操作更自然。视图是惰性求值的,不会立即产生中间容器,直到真正需要结果时才计算。常见视图包括 std::views::filter, std::views::transform, std::views::reverse 等。

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

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

    // 过滤偶数并乘以 10
    auto result = vec | std::views::filter([](int x){ return x % 2 == 0; })
                      | std::views::transform([](int x){ return x * 10; });

    for (auto v : result) std::cout << v << ' ';
    std::cout << '\n'; // 输出: 20 40 60 80
}

3. 将 std::spanstd::ranges 结合使用

结合 std::spanstd::ranges 可以实现对数组的切片、过滤、变换,且不产生额外拷贝。

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

int main() {
    int data[] = {3, 7, 1, 4, 9, 2, 5};

    // 创建 span,取下标 2 到 6 的子段
    std::span <int> sub(data + 2, 4); // {1, 4, 9, 2}

    // 过滤出偶数并累加
    int sum = std::accumulate(
        sub | std::views::filter([](int x){ return x % 2 == 0; }),
        0);

    std::cout << "偶数之和: " << sum << '\n'; // 输出: 6
}

小技巧

  • 使用 sub | std::views::take(n) 可以进一步限定长度。
  • sub | std::views::reverse 可以逆序访问。
  • std::ranges::sort(sub) 可以直接对 span 进行原地排序。

4. 性能与安全性

  • 无拷贝std::span 本身不复制数据,视图仅指向原数据。结合视图的惰性求值,整个链式操作在内部只扫描一次,几乎没有额外开销。
  • 边界检查:与 std::arraystd::vector 的索引操作相比,std::span 在访问时不会自动检查越界(因为它是一个视图),但如果你使用 std::span::at(),则会抛出异常。需要根据场景自行决定是否开启。
  • 生命周期std::span 只安全用于在其生命周期内有效的数据。不要把指向局部数组的 span 返回给外部。

5. 进阶:自定义视图

如果你想实现更复杂的切片规则(如步长、窗口滑动),可以自定义视图:

namespace myviews {
    struct step_view : std::ranges::view_interface <step_view> {
        std::span <int> base_;
        std::size_t step_;
        auto begin() const { return base_.begin(); }
        auto end() const { return base_.end(); }
        // 你可以在此实现自定义迭代器
    };
}

这在需要非连续步进访问(如取每隔两个元素)时非常有用。

6. 结语

std::spanstd::ranges 的组合提供了一种现代、类型安全、无拷贝的数据处理方式。无论是日常的数组切片,还是复杂的过滤和变换,利用它们都能写出更简洁、更易维护的 C++ 代码。建议在新项目中立即启用 C++20 标准,并尝试将旧代码迁移到基于 spanranges 的实现方式,长期来看将获得可读性和性能的双重收益。

发表评论