C++20 中的 std::span 与传统指针的比较

在 C++20 之前,C++ 代码中处理连续容器片段的方式大多是使用原始指针配合长度信息。随着 std::span 的加入,标准化了这一操作模式,提供了更安全、更易读的接口。本文将从定义、使用场景、性能以及兼容性四个方面对比 std::span 与传统指针,帮助你在项目中做出更合适的选择。


1. 基本定义

对象 关键字 语义
std::span std::span<T, N> 逻辑上等价于 T* ptrsize_t len 的组合;模板参数 N 可指定常量大小,默认为 dynamic_extent(运行时可变)
指针 T* ptr 只保存地址,缺乏长度信息

std::span 本质上是一个轻量级的“视图”对象,持有指针和长度,且本身不拥有底层数据,避免了额外的内存开销。


2. 使用场景对比

2.1 只读访问

void process(const std::span<const int>& data) {
    for (int v : data) std::cout << v << ' ';
}

相比:

void process(const int* data, std::size_t len) {
    for (std::size_t i = 0; i < len; ++i) std::cout << data[i] << ' ';
}
  • 可读性const std::span<const int>& 一眼即可知函数接受的是一个只读的整数序列。
  • 安全性:编译器可对 span 的范围进行边界检查(在某些实现中),减少越界风险。

2.2 可变访问

void increment(std::span <int> data) {
    for (int& v : data) ++v;
}

传统方式:

void increment(int* data, std::size_t len) {
    for (std::size_t i = 0; i < len; ++i) ++data[i];
}
  • 语义统一:只读与可变使用同一个模板,区别在于 `span ` 与 `span`。
  • 避免指针误用span 的引用传递可防止忘记传递长度。

2.3 与标准容器互操作

std::vector <int> vec{1,2,3,4,5};
process(std::span<const int>(vec));          // 隐式转换
process(vec);                                 // 直接传递,隐式转换为 span

传统指针则需手动取地址:

process(vec.data(), vec.size());

3. 性能与内存

std::span 的实现通常是一个包含指针和大小的 POD 结构:

struct span {
    T* ptr;
    std::size_t size;
};

这与两根指针(T* begin, T* end)或指针+长度的组合在内存占用上没有区别。拷贝成本同样极低,只有几字节。

关键的性能差异在于 函数调用的接口

  • span 可以作为参数、返回值、成员变量传递,符合现代 C++ 的设计理念。
  • 传统指针+长度组合往往导致接口繁琐,需要额外的文档说明参数顺序。

4. 兼容性与迁移

兼容性 说明
旧编译器 std::span 需要 C++20 或支持的后向兼容库(如 std::experimental::span
第三方库 大多数现代库已接受 span 作为参数类型;若使用旧库,仍可通过 T*size_t 的适配器实现
内存安全 span 本身不管理内存,仍需保证底层数据在 span 生命周期内有效

迁移时的常见做法是先引入 std::span 作为新的接口,然后为旧接口提供包装函数:

void process_old(const int* data, std::size_t len) {
    process(std::span<const int>(data, len));
}

5. 小结

  • 安全std::span 明确表示“视图”,可在 IDE/编译器层面更好地追踪错误。
  • 简洁:接口统一,减少了函数签名的冗余。
  • 性能:与传统指针+长度等价,且易于编译器优化。

在新项目或对代码可读性、维护性有较高要求的场景,建议优先使用 std::span。在需要兼容旧编译器或已有大量基于指针的 API 时,保持传统方式亦可行,且可通过包装层逐步过渡。

发表评论