¿Qué es un "span" y cuándo debo utilizarlo?
Recientemente recibí sugerencias para usar span<T>
's en mi código, o he visto algunas respuestas aquí en el sitio que usan span
's, supuestamente algún tipo de contenedor. Pero no puedo encontrar nada parecido en la biblioteca estándar de C++17.
Entonces, ¿qué es este misterio span<T>
y por qué (o cuándo) es una buena idea utilizarlo?
¿Qué es?
Una span<T>
es:
- Una abstracción muy ligera de una secuencia contigua de valores de tipo
T
en algún lugar de la memoria. - Básicamente
struct { T * ptr; std::size_t length; }
con un montón de métodos convenientes. - Un tipo que no es propietario (es decir, un "tipo de referencia" en lugar de un "tipo de valor"): nunca asigna ni desasigna nada y no mantiene vivos los punteros inteligentes.
Anteriormente se conocía como array_view
e incluso antes como array_ref
.
¿Cuándo debo usarlo?
Primero, cuándo no usar tramos:
- No use un intervalo en código que podría tomar cualquier par de iteradores de inicio y fin (como
std::sort
, y otras funciones con plantilla de ), y tampoco en código que toma un rango arbitrario (consulte La biblioteca de rangos de C++20std::find_if
para información sobre aquellos). Un intervalo tiene requisitos más estrictos que un par de iteradores o un rango: contigüidad de elementos y presencia de elementos en la memoria.std::copy
<algorithm>
- No utilice un intervalo si tiene un contenedor de biblioteca estándar (o un contenedor Boost, etc.) que sabe que es el adecuado para su código. Los tramos no están destinados a reemplazar los contenedores existentes.
Ahora, para saber cuándo utilizar realmente un intervalo:
Utilice
span<T>
(respectivamente,span<const T>
) en lugar de uno independienteT*
(respectivamenteconst T*
) cuando la longitud o el tamaño asignados también sean importantes. Entonces, reemplace funciones como:
void read_into(int* buffer, size_t buffer_size);
con:
void read_into(span<int> buffer);
¿Por qué debería usarlo? ¿Por qué es algo bueno?
¡Oh, los tramos son increíbles! Usando un lapso...
significa que puede trabajar con esa combinación de puntero+longitud/puntero de inicio+fin como lo haría con un contenedor de biblioteca estándar sofisticado y mejorado, por ejemplo:
for (auto& x : my_span) { /* do stuff */ }
std::find_if(my_span.cbegin(), my_span.cend(), some_predicate);
std::ranges::find_if(my_span, some_predicate);
(en C++20)
... pero sin absolutamente ninguno de los gastos generales en los que incurren la mayoría de las clases de contenedores.
A veces permite que el compilador haga más trabajo por usted. Por ejemplo, esto:
int buffer[BUFFER_SIZE]; read_into(buffer, BUFFER_SIZE);
se convierte en esto:
int buffer[BUFFER_SIZE]; read_into(buffer);
... que hará lo que usted quisiera que hiciera. Véase también la pauta P.5 .
es la alternativa razonable a pasar
const vector<T>&
a funciones cuando espera que sus datos sean contiguos en la memoria. ¡Ya no serás regañado por los poderosos gurús de C++!facilita el análisis estático, por lo que el compilador podría ayudarle a detectar errores tontos.
permite la instrumentación de compilación de depuración para la verificación de límites en tiempo de ejecución (es decir,
span
los métodos tendrán algún código de verificación de límites dentro de#ifndef NDEBUG
...#endif
)indica que su código (que usa el intervalo) no posee la memoria apuntada.
Hay aún más motivación para usar span
s, que puede encontrar en las pautas básicas de C++ , pero se da cuenta.
¿Pero está en la biblioteca estándar?
editar: Sí, std::span
se agregó a C++ con la versión C++20 del lenguaje.
¿Por qué sólo en C++20? Bueno, si bien la idea no es nueva, su forma actual se concibió junto con el proyecto de directrices centrales de C++ , que recién comenzó a tomar forma en 2015. Así que tomó un tiempo.
Entonces, ¿cómo lo uso si estoy escribiendo C++17 o una versión anterior?
Es parte de la Biblioteca de soporte de las Directrices básicas (GSL). Implementaciones:
- GSL de Microsoft/Neil Macintosh contiene una implementación independiente:
gsl/span
- GSL-Lite es una implementación de encabezado único de todo el GSL (no es tan grande, no te preocupes), incluido
span<T>
.
La implementación de GSL generalmente supone una plataforma que implementa soporte para C++14 [ 12 ]. Estas implementaciones alternativas de encabezado único no dependen de las instalaciones de GSL:
martinmoene/span-lite
requiere C++98 o posteriortcbrindle/span
requiere C++ 11 o posterior
Tenga en cuenta que estas diferentes implementaciones de intervalos tienen algunas diferencias en los métodos/funciones de soporte con las que vienen; y también pueden diferir algo de la versión adoptada en la biblioteca estándar en C++20.
Lectura adicional: Puede encontrar todos los detalles y consideraciones de diseño en la propuesta oficial final antes de C++ 17, P0122R7: span: vistas con límites seguros para secuencias de objetos de Neal Macintosh y Stephan J. Lavavej. Aunque es un poco largo. Además, en C++20, la semántica de comparación de intervalos cambió (después de este breve artículo de Tony van Eerd).
Una span<T>
es esta:
template <typename T>
struct span
{
T * ptr_to_array; // pointer to a contiguous C-style array of data
// (which memory is NOT allocated nor deallocated
// nor in any way managed by the span)
std::size_t length; // number of elements of type `T` in the array
// Plus a bunch of constructors and convenience accessor methods here
}
Es un contenedor liviano alrededor de una matriz de estilo C, preferido por los desarrolladores de C++ siempre que usan bibliotecas de C y desean envolverlas con un contenedor de datos de estilo C++ para "seguridad de tipos", "estilo C++" y "sensación de bienestar". ". :)
Nota: Llamo al contenedor de estructuras definido anteriormente, conocido como span, un "envoltorio liviano alrededor de una matriz de estilo C" porque apunta a una pieza de memoria contigua, como una matriz de estilo C, y la envuelve con métodos de acceso y el tamaño de la matriz. Esto es lo que quiero decir con "envoltorio ligero": es un envoltorio alrededor de un puntero y una variable de longitud, además de funciones.
Sin embargo, a diferencia de a std::vector<>
y otros contenedores estándar de C++, que también pueden tener tamaños de clase fijos y contener punteros que apuntan a su memoria de almacenamiento, un span no es propietario de la memoria a la que apunta y nunca la eliminará ni cambiará su tamaño ni asignará nueva memoria automáticamente. Nuevamente, un contenedor como un vector posee la memoria a la que apunta y la administrará (asignará, reasignará, etc.), pero un intervalo no posee la memoria a la que apunta y, por lo tanto, no la administrará.
Ir más lejos:
@einpoklum hace un buen trabajo al presentar lo que span
es a en su respuesta aquí . Sin embargo, incluso después de leer su respuesta, es fácil para alguien nuevo en spans tener todavía una secuencia de preguntas que no están completamente respondidas, como las siguientes:
- ¿ En qué se
span
diferencia una matriz C? ¿Por qué no usar uno de esos? Parece que es uno de los que también tienen el tamaño conocido... - Espera, eso suena como
std::array
, ¿en qué sespan
diferencia de eso? - Oh, eso me recuerda, ¿no es
std::vector
asístd::array
también? - Estoy tan confundida. :( Qué es un
span
?
Entonces, aquí hay algo de claridad adicional al respecto:
CITA DIRECTA DE SU RESPUESTA, CON MIS ADICIONES y comentarios entre paréntesis EN NEGRITA y mi énfasis en cursiva :
¿Qué es?
Una
span<T>
es:
- Una abstracción muy ligera de una secuencia contigua de valores de tipo
T
en algún lugar de la memoria.- Básicamente, una única estructura
{ T * ptr; std::size_t length; }
con varios métodos convenientes. (Observe que esto es claramente diferentestd::array<>
porque aspan
habilita métodos de acceso convenientes, comparables astd::array
, a través de un puntero al tipoT
y longitud (número de elementos) de typeT
, mientras questd::array
es un contenedor real que contiene uno o más valores de typeT
.)- Un tipo que no es propietario (es decir, un "tipo de referencia" en lugar de un "tipo de valor"): nunca asigna ni desasigna nada y no mantiene vivos los punteros inteligentes.
Anteriormente se conocía como
array_view
e incluso antes comoarray_ref
.
Esas partes en negrita son fundamentales para la comprensión, ¡así que no las pierda ni las malinterprete! A span
NO es una matriz C de estructuras, ni es una estructura de una matriz C de tipo T
más la longitud de la matriz (esto sería esencialmente lo que es el std::array
contenedor ), NI es una matriz C de estructuras de punteros para escribir T
más la longitud, sino que es una estructura única que contiene un único puntero para escribirT
y la longitud , que es el número de elementos (de tipo T
) en el bloque de memoria contiguo al que T
apunta el puntero para escribir. De esta manera, la única sobrecarga que ha agregado al usar a span
son las variables para almacenar el puntero y la longitud, y cualquier función de acceso conveniente que utilice y que span
proporcione.
Esto es DISTINTO a std::array<>
porque std::array<>
en realidad asigna memoria para todo el bloque contiguo, y es DISTINTO std::vector<>
porque a std::vector
es básicamente solo a std::array
que también crece dinámicamente (generalmente duplica su tamaño) cada vez que se llena y tratas de agregarle algo más. . A std::array
tiene un tamaño fijo y ni span
siquiera administra la memoria del bloque al que apunta, solo apunta al bloque de memoria, sabe cuánto dura el bloque de memoria, sabe qué tipo de datos hay en una matriz C en la memoria y proporciona funciones de acceso convenientes para trabajar con los elementos en esa memoria contigua .
Es parte del estándar C++:
std::span
es parte del estándar C++ a partir de C++20. Puede leer su documentación aquí: https://en.cppreference.com/w/cpp/container/span . Para ver cómo usar Google absl::Span<T>(array, length)
en C++11 o posterior hoy , consulte a continuación.
Descripciones resumidas y referencias clave:
std::span<T, Extent>
(Extent
= "el número de elementos en la secuencia, ostd::dynamic_extent
si es dinámico". Un intervalo simplemente apunta a la memoria y facilita el acceso, ¡pero NO la administra!):- https://en.cppreference.com/w/cpp/container/span
std::array<T, N>
(¡fíjate que tiene un tamaño fijoN
!):- https://en.cppreference.com/w/cpp/container/array
- http://www.cplusplus.com/reference/array/array/
std::vector<T>
(crece de tamaño automáticamente y dinámicamente según sea necesario):- https://en.cppreference.com/w/cpp/container/vector
- http://www.cplusplus.com/reference/vector/vector/
¿ Cómo puedo utilizar span
C++ 11 o posterior hoy ?
Google ha abierto sus bibliotecas internas de C++ 11 en forma de su biblioteca "Abseil". Esta biblioteca está destinada a proporcionar funciones de C++14 a C++20 y posteriores que funcionan en C++11 y versiones posteriores, para que pueda utilizar las funciones del mañana hoy. Ellos dicen:
Compatibilidad con el estándar C++
Google ha desarrollado muchas abstracciones que coinciden o se acercan mucho a las características incorporadas en C++14, C++17 y más allá. El uso de las versiones Abseil de estas abstracciones le permite acceder a estas funciones ahora, incluso si su código aún no está listo para funcionar en un mundo posterior a C++11.
Aquí hay algunos recursos y enlaces clave:
- Sitio principal: https://abseil.io/
- https://abseil.io/docs/cpp/
- Repositorio de GitHub: https://github.com/abseil/abseil-cpp
span.h
encabezado yabsl::Span<T>(array, length)
clase de plantilla: https://github.com/abseil/abseil-cpp/blob/master/absl/types/span.h#L153
Otras referencias:
- Estructura con variables de plantilla en C++
- Wikipedia: clases de C++
- visibilidad predeterminada de los miembros de clase/estructura de C++
Relacionado:
- [otra de mis respuestas sobre plantillas y tramos] Cómo hacer tramos de tramos
La respuesta proporcionada por Einpoklum es excelente, pero tuve que profundizar en la sección de comentarios para comprender un detalle específico, por lo que esto es una extensión para aclarar ese detalle.
Primero, cuándo no usarlo:
No lo use en código que podría tomar cualquier par de iteradores de inicio y fin, como std::sort, std::find_if, std::copy y todas esas funciones con plantilla súper genéricas. No lo use si tiene un contenedor de biblioteca estándar (o un contenedor Boost, etc.) que sabe que es el adecuado para su código. No pretende suplantar a ninguno de ellos.
Cualquier par de iteradores de inicio y fin en contraposición a los punteros de inicio y fin del almacenamiento continuo.
Como alguien que rara vez entra en contacto con el interior de los iteradores, se me escapó durante la lectura de la respuesta que los iteradores podían iterar sobre una lista vinculada, lo que los punteros simples (y un intervalo) no podían.