C++20 Span 简介

C++20 引入了 std::span 类模板, 它是一个轻量级的, 非拥有的范围视图, 用于表示连续的内存区域. std::span 可以用于数组, std::vector, std::string 等数据结构, 提供了一种统一的方式来访问这些数据结构的元素.

std::span 首先不是一个容器, 而是一个视图, 它不拥有数据, 只是对数据的引用. 这使得 std::span 更加高效, 因为它不需要复制数据. std::span 可以用于函数参数, 允许函数接受任意大小的数组或容器, 而无需复制数据.

本文将介绍 std::span 的基本用法和一些示例.

创建 std::span

std::span 可以通过多种方式创建.在声明时指定了元素数量的是固定 span, 未指定大小的是动态 span. 动态 span 的大小可以随着对应容器大小的变化而变化, 而固定 span 的大小是固定的.

#include <fmt/core.h>
#include <fmt/ranges.h>

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

int main() {
  std::vector<int> v{1, 2, 3, 4, 5};
  // 从vector容器创建 span
  auto s1 = std::span{v};
  fmt::println("s1: {}", s1);  // s1: [1, 2, 3, 4, 5]

  // 使用指针和大小创建 span
  auto s2 = std::span{v.data(), 3};
  fmt::println("s2: {}", s2);  // s2: [1, 2, 3]

  // 从view创建 span
  auto s3 = std::views::counted(v.begin() + 2, 3);
  fmt::println("s3: {}", s3);  // s3: [3, 4, 5]

  // 从数组创建 span
  int arr[] = {1, 2, 3, 4, 5};
  auto s4 = std::span{arr + 1, 3};
  fmt::println("s4: {}", s4);  // s4: [2, 3, 4]

  // 从std::array创建 span
  std::array<int, 5> arr2{1, 2, 3, 4, 5};
  auto s5 = std::span{arr2};
  fmt::println("s5: {}", s5);  // s5: [1, 2, 3, 4, 5]

  // 从span构建sub span
  auto s6 = s5.subspan(1, 3);
  fmt::println("s6: {}", s6);  // s6: [2, 3, 4]
}

std::span 的操作

可以使用 firstlast 方法获取 span 的前几个或后几个元素; 这里有两个版本:

  1. first<N>()last<N>() 返回一个新的 span, 包含前 N 个或后 N 个元素. 是一个模板函数.
  2. first(N)last(N) 返回一个新的 span, 包含前 N 个或后 N 个元素.
  3. subspan(pos, count) 返回一个新的 span, 包含从位置 pos 开始的 count 个元素.
#include <fmt/core.h>
#include <fmt/ranges.h>

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

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

  auto sp1 = s.first<2>();
  fmt::println("sp1: {}", sp1);  // sp1: [1, 2]

  auto sp2 = s.last<2>();
  fmt::println("sp2: {}", sp2);  // sp2: [5, 6]

  auto sp3 = s.first(2);
  fmt::println("sp3: {}", sp3);  // sp3: [1, 2]

  auto sp4 = s.last(2);
  fmt::println("sp4: {}", sp4);  // sp4: [5, 6]

  auto sp5 = s.subspan(2, 3);
  fmt::println("sp5: {}", sp5);  // sp5: [3, 4, 5]
}

std::span 注意事项

  1. std::span 不拥有数据, 只是对数据的引用, 因此在 std::span 存在时, 数据必须保持有效.
  2. std::span 如果 span 的底层数据被销毁, 那么 span 将变成悬垂指针, 这是一种未定义行为.
#include <fmt/core.h>
#include <fmt/ranges.h>

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

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

  auto current = vec.capacity();

  // 修改容器可能会导致 span 变成悬垂指针
  for (auto i = 7; i < 10; i++) {
    vec.push_back(i);
  }

  // 如果数组底层存储发生变化, 则应该重新创建 span
  if (current != vec.capacity()) {
    s = vec;
  }
  fmt::print("{}\n", s);
}

总结

可以看出std::spanstd::views有很多相似之处, 实际上, std::spanstd::views的一个特例. 关于 std::views 的更多内容可以查看我之前的博客: Modern C++ Ranges/View 库简介.