Saltar a contenido

Última actualización:2026-10-12

Diseño y trampas de std::span en C++20

0.Motivación

En el código C/C++ tradicional, solemos usar un puntero + un tamaño para acceder a un array, por ejemplo:

void print(int* arr, int sz) {
    for (int i = 0; i < sz; i++) {
        // todo
    }
}

span simplifica esta operación, que queda así:

void print(std::span<int> arr) {
    for (auto x : arr) {
        // todo
    }
}

Entonces, ¿qué es exactamente un span, cuáles son sus escenarios de uso y cuáles sus trampas? Esta sección lo explora en detalle.

1. ¿Qué es un span?

En C++20, span es un contenedor ligero de tipo rango que proporciona acceso a un tramo de memoria contigua. Como vista (view), span no posee sus datos, sino que es una simple referencia a ellos, y se usa habitualmente en sustitución de los arrays o punteros tradicionales de C. Puede alojar arrays de cualquier tipo y contenedores estándar, como std::vector, y ofrece una forma más segura y moderna de procesar estos datos.

Varios puntos clave:

  • Memoria contigua
  • No posee los datos
  • Referencia

El código fuente de span se encuentra en gcc, en libstdc++-v3/include/std/span.

template <typename _Type, size_t _Extent = dynamic_extent>
class span {};

El tipo span incluye dos parámetros importantes:

  • _Type: el tipo de los elementos.
  • Extent: el tamaño del array. Si es dynamic_extent (el valor por defecto), significa que el tamaño de la vista puede variar dinámicamente.

2. El span dinámico y el estático

span tiene dos modos: estático y dinámico.

2.1 span estático

El span estático determina su tamaño en tiempo de compilación; su Extent es un valor constante. Al usar un span estático, necesitamos conocer el tamaño de los datos en tiempo de compilación.

Por ejemplo:

int arr[5] = {1, 2, 3, 4, 5};
std::span<int, 5> s(arr);  // 静态 span,大小为 5

El tamaño del span estático queda fijado en tiempo de compilación, por lo que el compilador puede aprovecharlo para algunas optimizaciones, como omitir la comprobación de límites. Pero como Extent es constante, el span estático no puede modificar su tamaño en tiempo de ejecución.

2.2 span dinámico

El tamaño del span dinámico se determina en tiempo de ejecución; Extent es por defecto dynamic_extent. Este tipo de span no exige conocer el tamaño en tiempo de compilación y puede adaptarse a arrays o contenedores de cualquier tamaño.

Por ejemplo:

int arr[5] = {1, 2, 3, 4, 5};
std::span<int> s(arr);  // 动态 span,大小在运行时确定

El span dinámico es adecuado para los casos en que el tamaño no puede determinarse en tiempo de compilación, y ofrece mayor flexibilidad.

Su implementación en el código fuente de gcc es bastante sencilla: por defecto es un entero enorme (el valor sin signo de -1), y el cálculo real se hace en la construcción.

inline constexpr size_t dynamic_extent = static_cast<size_t>(-1);

Por ejemplo: aquí se usa ranges::size para fijar el tamaño interno; consulta el código siguiente.

std::vector<int> data{1, 2, 3};
std::span<int> s(data);

Establece el tamaño dinámico.

span(ranges::data(__range), ranges::size(__range))

3. subspan

span proporciona una función miembro muy útil, subspan, que permite extraer una subvista de un span existente sin copiar datos. Resulta muy adecuado al procesar conjuntos de datos grandes, evitando costes de memoria innecesarios.

El método subspan() tiene dos sobrecargas:

  1. Una toma __offset y __count como parámetros, donde __offset es la posición inicial y __count el tamaño del subrango (por defecto dynamic_extent, es decir, hasta el final del span original).
  2. La otra no tiene parámetros y se implementa mediante plantillas.
constexpr auto
subspan() const noexcept
-> span<element_type, _S_subspan_extent<_Offset, _Count>()> {}

template<size_t _Offset, size_t _Count = dynamic_extent>
[[nodiscard]]
constexpr auto
subspan() const noexcept
-> span<element_type, _S_subspan_extent<_Offset, _Count>()> {}

Ejemplo 1:

std::span<int> s(arr);  
std::span<int> sub = s.subspan(1, 3);

Ejemplo 2:

std::span<int> s(arr);  
auto sub = s.subspan<2, 2>();

4. Trampas de uso

Aunque span es una herramienta muy útil, al usarlo hay varias trampas habituales que conviene tener en cuenta:

4.1 No dejes que un span referencie memoria ya liberada

El propio span no posee los datos; es solo una referencia a ellos. Por tanto, si la memoria referenciada por el span se libera (por ejemplo, el array original se destruye o sale del ámbito), acceder a ese span provocará comportamiento indefinido.

Por ejemplo:

std::span<int> create_span() {
    int arr[5] = {1, 2, 3, 4, 5};
    return std::span<int>(arr, 5);
}

// 错误:返回的 span 引用了栈上临时变量,函数返回后 arr 被销毁
auto s = create_span();

Para evitar este problema, asegúrate de que los datos referenciados por el span siguen siendo válidos durante todo su ciclo de vida. Puede considerarse mantener los datos en memoria asignada dinámicamente, o garantizar que el ciclo de vida de los datos coincida con el del span.

4.2 Problemas al cambiar el tamaño del array subyacente

La referencia de span a los datos subyacentes es directa, no una copia. Cuando los datos subyacentes cambian, el propio span no se entera. Por eso, al cambiar el tamaño del array o contenedor subyacente (por ejemplo, reasignando memoria), el span original puede quedar invalidado.

std::vector<int> vec = {1, 2, 3, 4, 5};
std::span<int> s(vec);  // s 引用 vec 的数据
// 改变底层容器的大小
vec.push_back(6);  // vec 大小变为 6

// 这里 s 仍然引用 vec 的数据,但 vec 可能已经重新分配了内存
// 这可能导致 s 引用失效或未定义行为

Temas relacionados

Comentarios