如何在 C++20 中安全使用 std::span 进行数组切片

在 C++20 之前,指针或引用常常被用来手工管理数组切片,但这很容易导致越界、悬空指针以及内存泄漏。C++20 引入的 std::span 为这些问题提供了一种轻量、无所有权的视图。本文将通过代码示例展示 std::span 的使用场景、优势以及最佳实践。

1. 什么是 std::span
std::span 是一个非所有权的数据结构,它包含一个指向连续存储区的指针和长度信息。它本身不管理内存,而是借用已有的数据结构(如数组、std::vector、C 风格数组等)提供一个统一的视图。

2. 基本用法

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

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

int main() {
    int arr[5] = {1,2,3,4,5};
    std::vector <int> vec = {10,20,30,40,50};

    std::span <int> span1(arr);          // 直接从数组构造
    std::span <int> span2(vec);          // 直接从 vector 构造

    printSpan(span1);   // 输出 1 2 3 4 5
    printSpan(span2);   // 输出 10 20 30 40 50
}

3. 切片(subspan)

span 提供 subspan 方法,可以方便地取子视图:

auto mid = span2.subspan(2);          // 从索引 2 开始,到末尾
auto part = span2.subspan(1, 3);      // 从索引 1 开始,长度 3

printSpan(mid);   // 输出 30 40 50
printSpan(part);  // 输出 20 30 40

4. 与 C 风格数组和指针的对比

传统做法:

void process(int* data, std::size_t len) { ... }

使用 span 可以直接传递 std::vector 或数组:

void process(std::span <int> data) { ... }

// 调用
process(vec);     // 自动转换
process(arr);     // 自动转换

5. 通过 std::arraystd::vector 和 C 风格数组的隐式转换

  • std::array:构造 span 时可直接使用 `std::span s = arr;`。
  • std::vector:`std::span s = vec;` 只在 `vector` 的数据未移动时安全。
  • C 风格数组:`std::span s = {arr, std::size(arr)};` 或者 `std::span s = arr;`(C++23 之后支持)

6. 安全性与异常

  • std::span 仅存储指针和长度;其生命周期与引用的底层对象相同。若底层对象被销毁,span 会悬空。
  • 在多线程环境下,如果一个线程修改底层数据,另一个线程读取,必须使用同步机制。
  • 由于 span 本身不抛异常,使用 subspan 时若参数越界会抛出 std::out_of_range,因此请在调用前检查尺寸。

7. 性能

  • std::span 只包含两字段(指针 + 长度),占用空间与指针相当,传递开销极小。
  • 由于没有所有权,编译器可进行更多优化,如把 span 直接作为内联参数。

8. 代码示例:实现一个通用求和函数

template <typename T>
T sum(std::span <T> s) {
    T total = T{};
    for (const auto& v : s) total += v;
    return total;
}

int main() {
    std::vector <int> vec = {1,2,3,4,5};
    int arr[] = {10,20,30};

    std::cout << "sum(vec) = " << sum(vec) << '\n';   // 15
    std::cout << "sum(arr) = " << sum(arr) << '\n';   // 60
}

9. 常见陷阱

  • 悬空 span:在 vector 重新分配时,已有 span 仍指向旧内存。
  • 多余复制std::span 只是一个视图,使用时要确保不把它当作所有权容器。
  • 长度检查:在使用 subspan 前最好检查 size(),避免抛出异常。

10. 结语
std::span 为 C++20 引入的一种轻量级、无所有权的数据视图,使得函数签名更简洁、调用更安全、代码更易读。掌握它的使用,能够让你在处理数组、向量以及其他连续容器时更加高效。希望本文能帮助你在日常项目中更好地利用 std::span

发表评论