C++20 中的 std::span 你需要知道什么?

std::span 是 C++20 标准库中引入的轻量级视图,用来描述一段连续内存区域,而不拥有它。它本质上是一个指针和长度的组合,允许你在不复制数据的前提下,对数组、vector、字符串等容器进行安全、可变或不可变的访问。下面从概念、实现、优势、局限和常见使用场景四个角度,系统性地梳理你在项目中遇到的关键问题。

1. 概念与语义

关键词 说明
View std::span 是一个“视图”,它不持有数据,只引用它。
Length span 内部维护一个长度,表示视图的大小。
Const / Mutable `std::span
表示可变视图,std::span` 表示只读视图。
Zero-sized 支持空 span(长度为 0),但指针可以是 nullptr 或任意值。
No ownership span 不会导致对象的生命周期延长。

因为 span 不拥有数据,所以它可以安全地与容器生命周期关联。把 span 当作参数传递给函数,函数只能在调用者生命周期内使用。

2. 实现细节

template<class ElementType, std::size_t Extent = dynamic_extent>
class span {
public:
    using element_type = ElementType;
    using value_type   = std::remove_cv_t <ElementType>;
    using size_type    = std::size_t;
    using difference_type = std::ptrdiff_t;
    using pointer        = ElementType*;
    using const_pointer  = const ElementType*;
    using reference      = ElementType&;
    using const_reference = const ElementType&;
    using iterator       = pointer;
    using const_iterator = const_pointer;

    constexpr span() noexcept : data_{nullptr}, size_{0} {}
    constexpr span(pointer ptr, size_type sz) noexcept : data_{ptr}, size_{sz} {}

    // 从数组创建
    template<std::size_t N>
    constexpr span(element_type (&arr)[N]) noexcept : data_{arr}, size_{N} {}

    // 从容器创建(容器必须支持 .data() 与 .size())
    template<class Container,
             std::enable_if_t<
                 std::is_convertible_v<decltype(std::declval<Container>().data()), pointer> &&
                 std::is_convertible_v<decltype(std::declval<Container>().size()), size_type>, int> = 0>
    constexpr span(Container& c) noexcept : data_{c.data()}, size_{c.size()} {}

    // 访问
    constexpr reference operator[](size_type i) const noexcept { return data_[i]; }
    constexpr size_type size() const noexcept { return size_; }
    constexpr pointer data() const noexcept { return data_; }

private:
    pointer data_;
    size_type size_;
};
  • Extent:若已知长度,在模板参数中指定;否则使用 dynamic_extent(默认值)表示长度由构造函数传入。使用 extent 可以让编译器做更多检查,例如 span<int, 5> s{arr} 必须保证 arr 长度为 5。

  • 构造器的选择:span 通过多种构造器兼容 C++17 及之前的数组、容器、指针+长度组合。对容器的要求非常宽松,只要满足 .data().size() 即可。

3. 使用优势

场景 优势
函数参数 std::span 可以取代 T* + size_t,让调用者明确传递的是一段可变/不可变的数据块。
性能 只传递指针和长度,没有拷贝;与 std::vector 相比,避免了 heap 分配。
安全性 span 具有范围检查(at()),不易出现悬空指针或越界。
互操作 std::arraystd::vectorstd::string_view 兼容性好,易于在不同容器间切换。
静态检查 extent 的使用可以在编译期捕获长度不匹配。

4. 局限与注意事项

  1. 生命周期
    span 本身不管理生命周期,传递给函数时一定要保证引用的对象在函数调用期间存活。若传递 span 给异步或延迟执行的代码,需自行管理生命周期。

  2. 非连续存储
    只能用于连续内存。若需要访问稀疏或链式结构,需要转换为 std::vector 或使用 std::span<std::optional<T>> 等方案。

  3. 写时复制(Copy-on-Write)
    对 span 的 operator[] 返回的是引用,直接修改会影响原对象。若想保持不可变性,需要使用 span<const T> 或拷贝。

  4. 可变长数组(VLA)
    C++ 标准不支持 VLA;若你想要动态长度的数组,可以先用 std::vector,再通过 std::span 提供给函数。

  5. 模板元编程
    使用 spanconstexpr 结合时要注意 constexpr 的可行性。自 C++20 起,span 及其成员已成为 constexpr,可以在编译期使用。

5. 常见使用案例

5.1 处理输入缓冲区

void process_bytes(std::span<const std::uint8_t> buffer) {
    for (auto byte : buffer) {
        // 处理字节
    }
}

5.2 简化 API 重载

void write_data(std::span<const std::uint8_t> data);
void write_data(const std::vector<std::uint8_t>& vec) { write_data(vec); }
void write_data(const std::array<std::uint8_t, N>& arr) { write_data(arr); }

5.3 结合算法

auto sorted = std::is_sorted(buf.data(), buf.data() + buf.size());

5.4 与 std::span 组合使用 std::bitset

void toggle_bits(std::span<std::uint32_t> words, std::size_t start_bit, std::size_t count) {
    for (std::size_t i = 0; i < count; ++i) {
        std::size_t bit = start_bit + i;
        std::size_t idx = bit / 32;
        std::size_t pos = bit % 32;
        words[idx] ^= static_cast<std::uint32_t>(1u << pos);
    }
}

6. 进阶技巧

  • 自定义范围:如果你想让类支持 for (auto v : obj),实现 begin()end() 并返回 span,即可把对象变成“可迭代范围”。

  • std::initializer_list 互通std::initializer_list 内部就是一个 span 的实现。你可以直接 `span

    {{1, 2, 3}}`。
  • 静态检查:利用 static_assert(sizeof...(Ns) == N) 来验证 span<T, N> 的长度。

  • 多维 span:C++23 里有 std::mdspan 用于多维数组视图,类似于 span 但支持二维以上。

7. 小结

  • std::span 是对连续内存的轻量级、无所有权的视图。
  • 它让函数接口更安全、更易读,同时不牺牲性能。
  • 使用时需谨慎管理对象生命周期,避免悬空引用。
  • 在现代 C++ 开发中,几乎所有需要传递数组、字符串、缓冲区的地方都能考虑用 std::span

掌握好 std::span,你会发现许多传统 C 风格 API 能被更优雅、更安全的 C++ 代码所取代。祝编码愉快!

发表评论