C++ 中的 std::span::subspan 函数

2025年5月14日 | 11 分钟阅读

std::span 类模板概述

std::span 类模板是 C++20 中引入的一个全新的结构,它是一个轻量级的、非拥有的对象序列指针。它提供了一种访问数组或其中一部分的方式,而无需像指针那样复制数据,同时具有更安全、更方便的语义。以下是详细概述:

关键特性

  1. 非拥有视图
    与 std:: 容器不同,span 并非容器,并且当它基于 C++ 容器(如 vector 或 std::array)时,span 并不拥有它所指向的数据。它仅仅是一个元素范围的视图,明确表示它不负责内存管理。
  2. 连续内存
    std::span 操作的是位于相邻内存中的序列,例如数组、std::vector 或 C 风格的数组。它的设计目的是操作内存中没有空隙的数据序列。
  3. 模板化以提高灵活性
    • std::span 是模板化的,这使得它能够与任何数据类型(整数、浮点数、用户定义的 数据类型 等)以及静态或动态大小的序列一起工作。
    • 静态范围:如果 span 的大小在编译时已知,std::span 可以是固定大小的。
    • 动态范围:如果大小直到运行时才知道,这对于 std::span 来说也不是问题。

使用 std::span 处理类数组数据的优势

在 C++ 中使用 std::span 处理类数组数据提供了与 C++ 自身不同的重要优势,包括安全性、清晰度和性能。

提高类型安全性

  • std::span 同时包含数据地址和数组大小,因此消除了诸如缓冲区溢出或越界访问之类的基本错误的可能性。
  • 使用原始指针时,您必须手动跟踪大小,而 std::span 始终使其可用,从而提高了安全性并减少了错误数量。

简化的函数参数

  • 处理类数组数据的函数通常需要两个独立的参数:指向数据的指针和指示可用数据量以及传递给函数的数量的数组大小。std::span 将两者合并为一个参数,使传递更简单。
  • 这不仅使函数签名更简洁、更易于阅读,而且还防止了不一致(例如,传递错误的数组大小)。

示例

其他非专业视图(无内存管理考虑)

  • std:: span 不拥有其引用的特定内存的引用。它仅仅提供数据视图,这意味着没有内存分配或释放的开销。
  • 这使得 std::span 轻量且高效,尤其是在将大型数组/数据切片传递给函数时,因为它不会导致数据复制。

与标准容器的互操作性

  • std::span 可以从多种类型的容器、原始数组、std::array、std::vector 的 span,甚至 std::initializer_list 构建。
  • 这种多功能性使得 std::span 非常适合编写现代 C++ 代码、维护旧代码以及连接它们。

示例

无复制或性能开销

  • 使用 std::span 将类数组数据结构传递给函数时,不会复制任何数据。它只是一个指向现有内存的 span;不会对性能产生影响。
  • 这使得 std::span 在性能至关重要的情况下特别有用,尤其是在处理大型数据集时。

理解 std::span::subspan 函数

它是一个 span 创建实用函数,用于创建 std::span::subspan 或现有 std::span 的部分视图。此函数使您无需访问、更改或传输数据本身即可操作 span 所指示的数据的一部分。以下是该函数的详细描述、其参数及其工作原理:

subspan() 的定义和语法

subspan() 函数从现有的 std::span 中提取一个子 span,从指定偏移量开始,并可能包含给定数量的元素。

语法

它具有以下语法:

  • T: span 中元素的类型。
  • Extent: span 的大小(静态或动态)。

该函数创建一个新的 std::span 对象,该对象表示从偏移索引开始的视图,并且可选地包含 count 个元素。

示例

subspan 的参数:Offset 和 Count

Offset

  • 类型: std::size_t
  • 描述: 这是索引(对应于原始 span 中的起始索引,新 subspan 的起始索引为 0)。偏移量之前的所有元素也不会包含在新生成的 subspan 中。

Count(可选)

  • 类型: std::size_t(可选;默认值为 std::dynamic_extent)
  • 描述: 这是从偏移量开始在 subspan 中包含的元素数量。如果省略 count(如上所示,这是一种常见做法),则 subspan 将从偏移量开始一直到 span 的末尾。
  • 默认值: 如果省略 count,则 subspan 将从偏移量延伸到 span 的最终位置。

示例

返回值和行为

返回类型:std::span<T>

该函数返回一个新的 std::span 对象,该对象表示原始 span 中元素的一个子集。

行为

  • Offset 行为: subspan 从给定偏移量指示的元素开始定义。如果偏移量大于原始 span 的大小,则会产生未定义行为。
  • Count 行为: 如果 count 大于从偏移量开始可用的元素数量,它将返回一个 span,其中包含原始 span 中剩余的元素。
  • 零成本抽象: 与 std::span 一样,subspan 不被认为会分配任何内存或在视图之间复制元素。它仅提供对初始数据一部分的全新视角。

边缘情况

  • 越界访问: 关于导致超出 span 大小的偏移量或计数,其行为是未声明的。但是,如果编译器设置了特定的调试标志,则可能会进行运行时边界检查。
  • 空 Span: 如果 count 为 0 或 subspan 超出了数据范围,则新 span 将为空 span。

越界行为示例

使用 std::span::subspan 的示例

以下是一些演示如何使用 std::span::subspan 函数的示例,涵盖了基本用法和更具体的情况。

示例 1:创建 subspan 的基本示例

此示例从一个 数组 创建一个 std::span,然后使用偏移量和计数提取一个 subspan。

输出

 
30 40 50   

说明

  • 我们创建了一个包含 6 个元素的数组的 std::span。
  • 使用 subspan(2, 3),我们提取一个从索引 2 开始并包含 3 个元素的 subspan,得到 {30, 40, 50}。

示例 2:仅带偏移量的 Subspan

当您仅提供偏移量时,subspan 将从指定偏移量到原始 span 末尾创建一个视图。

输出

 
4 5 6   

说明

  • 我们创建了一个 span 来覆盖数组 {1, 2, 3, 4, 5, 6}。
  • 通过使用 subspan(3),我们创建了一个从索引 3 开始并包含从那里到末尾所有元素的 subspan,结果为 {4, 5, 6}。

示例 3:同时带有偏移量和计数的 Subspan

此示例通过指定偏移量和计数来创建 subspan,限制新 subspan 中的元素数量。

输出

 
10 15 20 25   

说明

  • 原始 span 覆盖数组 {5, 10, 15, 20, 25, 30, 35} 中的所有元素。
  • 使用 subspan(1, 4),我们从索引 1 开始并包含 4 个元素,得到 subspan {10, 15, 20, 25}。

std::span::subspan 中的错误处理

1. 处理越界错误

默认情况下,std::span::subspan 方法不对偏移量值或相对于底层数据的计数执行任何范围检查。如果您提供了不正确的偏移量或请求的项目多于允许的数量,结果不是错误;而是会导致不可预测的行为。

越界错误示例

  • 偏移量过大: 当偏移量大于 span 的大小时,会发现可以访问越界内存。
  • 计数过大: 当计数(请求的元素数量)超过从偏移量开始由 span 定义的空间量时,这种情况也会导致未定义行为。

示例

说明

  • 在第一种情况下,span.subspan(6) 尝试从不存在的索引开始一个 subspan,这可能导致未定义行为(崩溃或数据错误)。
  • 在第二种情况下,span.subspan(2, 2) 可以安全运行,因为偏移量和计数都包含在 span 的边界内。

如何避免越界访问

手动验证偏移量和计数:由于 subspan() 默认不检查边界,因此您应该执行手动检查以确保偏移量和计数在有效范围内。

使用编译器标志或调试库: 某些编译器或环境提供有助于在调试构建中捕获越界访问的工具。这些运行时检查通常可用于 -D_GLIBCXX_DEBUG (GCC) 或 -D_LIBCPP_DEBUG (libc++) 等标志,这些标志启用了额外的安全检查。

2. 理解运行时检查(当 subspan 抛出异常时)

尽管 std::span::subspan 不会自动抛出异常,但某些编译器可以在调试版本中或在设置了某些特殊标志时生成异常。这些运行时检查对于防止越界访问大有帮助,从而消除了未定义行为的可能性。

发生运行时检查时

  • 调试构建: 大多数典型的库(例如 libc++ 或 libstdc++)在调试模式下提供边界检查。如果您尝试使用无效的偏移量或计数创建 subspan,程序将抛出异常,通常是 std::out_of_range。
  • 编译器标志: 某些编译器在启用指定的调试标志进行编译时,会执行边界检查并在访问冲突时引发异常。

启用运行时检查示例(GCC)

在 GCC 中,您可以通过使用 -D_GLIBCXX_DEBUG 进行编译来启用运行时检查

g++ -D_GLIBCXX_DEBUG my_program.cpp -o my_program

这将启用检查,如果创建了无效的 span,则会抛出异常(例如 std::out_of_range)。

带运行时检查的示例

说明

  • 我们首先检查请求的偏移量和计数是否有效,然后再创建 subspan。
  • 如果它们越界,我们将避免调用 subspan() 并处理错误,而不是让未定义行为发生。

带调试标志的运行时检查示例

如果您使用的是提供边界检查的编译器或环境(例如带 -D_GLIBCXX_DEBUG 的 GCC),如果边界被违反,相同的代码将自动抛出异常。

结论

总之,C++ 中的 std::span 类模板通过提供非拥有的、轻量级的连续内存视图,为处理多元素 对象 提供了一种更安全、更高效的方式。std::span::subspan 功能使用户能够在不使用额外数据副本的情况下从现有 span 创建子 span,同时确保值的安全性和清晰度。

但是,由于 std::span::subspan 没有内置的保护机制来防止访问越界数据,因此用户在使用偏移量和计数时必须小心。根据输入,程序员可以验证所有输入或打开运行时检查(例如,在某些编译器中打开调试标志),从而使代码更安全并消除未定义行为。

要点

  • 非拥有且轻量级: std::span 在数据处理方面非常出色,无需复制输入数据。
  • 改进的安全性: 虽然它不拥有内存,但 std::span 包装了指针和大小,从而防止了缓冲区溢出等问题。
  • 需要手动验证: 因此,应手动约束偏移量和计数,以避免越界,或在调试配置中使用运行时断言。