Un std::span [C++20] es un objeto ligero que representa una vista no propietaria sobre una secuencia contigua de objetos de tipo T. Internamente, no es más que un par de valores: un puntero al inicio de la secuencia y un tamaño. Al no ser un contenedor, no asigna ni libera memoria; su única función es proporcionar una interfaz estandarizada para iterar y acceder a datos que ya existen en algún lugar (ya sea en el stack, en el heap o en un segmento de datos estáticos).
Esta arquitectura responde a la necesidad de desacoplar la lógica de procesamiento de la estructura de almacenamiento. En lugar de sobrecargar una función para que acepte un std::vector<T>, un std::array<T, N> o un array de estilo C, utilizas std::span<T>. Esto permite que tu API sea extremadamente flexible sin pagar el coste de copiar datos.
Existen dos variantes fundamentales según su extensión (el tamaño de la vista):
1. Extensión estática: std::span<int, 5> tiene su tamaño definido en tiempo de compilación. Esto permite al compilador optimizar el acceso y eliminar comprobaciones de límites en ciertos contextos.
2. Extensión dinámica: std::span<int> almacena el tamaño en tiempo de ejecución, siendo la forma más común para trabajar con contenedores dinámicos como std::vector.
Debes usar std::span siempre que diseñes funciones que necesiten leer o modificar una secuencia de elementos contiguos sin importar qué contenedor los contiene. Es el sucesor semántico de std::string_view [C++17], pero mientras que string_view está especializado para secuencias de caracteres, span es genérico para cualquier tipo T.
Ten mucho cuidado: como std::span no es el dueño de los datos, si el contenedor original (el dueño) es destruido o se reasigna (causando un realloc en el caso de un std::vector), el span quedará invalidado, apuntando a memoria que ya no pertenece a tu programa.
#include <iostream>
#include <vector>
#include <array>
#include <span>
#include <numeric>
#include <algorithm>
// Una API moderna usa span para ser agnóstica al contenedor.
// Usamos const para garantizar que esta función no modifique los datos originales.
void imprimir_segmento(std::span<const float> datos, std::string_view etiqueta) {
if (datos.empty()) {
std::cout << etiqueta << ": vacío\n";
return;
}
std::cout << etiqueta << " (tamaño " << datos.size() << "): ";
for (float v : datos) {
std::cout << v << " ";
}
std::cout << "\n";
}
// Ejemplo de procesamiento de datos
void procesar_sensor_data(std::span<float> datos) {
// subspan(offset, count) crea una nueva vista sobre una porción de la original
// sin copiar el contenido, solo ajustando el puntero y el tamaño.
if (datos.size() < 3) return;
auto parte_relevante = datos.subspan(1, 2);
// first(n) y last(n) son formas convenientes de obtener sub-vistas.
auto primer_elemento = datos.first(1);
std::cout << "--- Procesando ---" << std::endl;
std::cout << "Primer elemento: " << primer_elemento[0] << std::endl;
std::cout << "Promedio de sub-segmento: " << std::accumulate(parte_relevante.begin(),
parte_relevante.end(), 0.0f) / 2.0f
<< std::endl;
}
int main() {
// 1. Desde un std::vector (Extensión dinámica)
std::vector<float> vec = {10.5f, 20.5f, 30.5f, 40.5f, 50.5f};
// 2. Desde un std::array (Extensión dinámica/estática según se use)
std::array<float, 3> arr = {1.0f, 2.0f, 3.0f};
// 3. Desde un array nativo de C
float raw_arr[] = {100.0f, 200.0f};
// El span puede construirse implícitamente desde contenedores estándar
imprimir_segmento(vec, "Vector");
imprimir_segmento(arr, "Array");
imprimir_segmento(raw_arr, "Raw Array");
// Trabajamos con una parte del vector
procesar_sensor_data(vec);
return 0;
}
Desglose del ejemplo
En la función imprimir_segmento, recibimos un std::span<const float>. Al usar const en el template, estamos diciendo que la vista es inmutable, aunque el contenedor original (como vec) sea mutable. Esto es una característica clave de las vistas: la restricción de acceso se aplica a la vista, no al objeto original.
Cuando llamamos a datos.subspan(1, 2) dentro de procesar_sensor_data, no estamos creando un nuevo vector o copiando elementos. El compilador simplemente genera un nuevo objeto std::span cuyo puntero interno es puntero_original + 1 y cuyo tamaño es 2. Es una operación $O(1)$.
Fíjate en la versatilidad de main: la misma función imprimir_segmento procesa un std::vector, un std::array y un array de estilo C. Esto es posible porque todos ellos exponen una secuencia contigua de memoria que el constructor de std::span puede interpretar. Si el usuario intenta pasar un std::list, el código no compilará, lo cual es una ventaja de seguridad: std::span garantiza por contrato que los datos están contiguos en memoria, permitiendo optimizaciones de CPU como el prefetching.
Si en el futuro necesitas trabajar con matrices multidimensionales de forma eficiente, mira hacia std::mdspan [C++23], que extiende este mismo concepto de vista a múltiples dimensiones.
El error frecuente
El error más peligroso con std::span es la creación de un dangling span al intentar devolver una vista de un objeto local.
// ERROR CRÍTICO: Retorna una vista a una memoria que desaparecerá
std::span<int> obtener_datos_peligrosos() {
std::vector<int> datos_locales = {1, 2, 3, 4, 5};
return std::span<int>(datos_locales);
} // <--- datos_locales se destruye aquí, el span queda invalidado.
int main() {
auto mi_vista = obtener_datos_peligrosos();
std::cout << mi_vista[0] << std::endl; // UNDEFINED BEHAVIOR
}
Este error es un caso de uso de memoria después de su liberación. El std::vector libera su buffer interno al salir del ámbito de la función. El std::span resultante contiene un puntero que apunta a esa memoria liberada. Este bug es silencioso y puede no fallar inmediatamente, pero causará corrupción de memoria o un crash aleatorio. Herramientas como AddressSanitizer (-fsanitize=address) detectarán esto instantáneamente al ejecutar el programa.
N° 119