¿Qué es un "span" y cuándo debo utilizarlo?

Resuelto einpoklum asked hace 7 años • 4 respuestas

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?

einpoklum avatar Aug 17 '17 05:08 einpoklum
Aceptado

¿Qué es?

Una span<T>es:

  • Una abstracción muy ligera de una secuencia contigua de valores de tipo Ten 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_viewe 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 independiente T*(respectivamente const 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, spanlos 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 spans, que puede encontrar en las pautas básicas de C++ , pero se da cuenta.

¿Pero está en la biblioteca estándar?

editar: Sí, std::spanse 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-literequiere C++98 o posterior
  • tcbrindle/spanrequiere 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).

einpoklum avatar Aug 16 '2017 22:08 einpoklum

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 spanes 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:

  1. ¿ En qué se spandiferencia una matriz C? ¿Por qué no usar uno de esos? Parece que es uno de los que también tienen el tamaño conocido...
  2. Espera, eso suena como std::array, ¿en qué se spandiferencia de eso?
  3. Oh, eso me recuerda, ¿no es std::vectorasí std::arraytambién?
  4. 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 Ten 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 diferente std::array<>porque a spanhabilita métodos de acceso convenientes, comparables a std::array, a través de un puntero al tipoT y longitud (número de elementos) de type T, mientras que std::arrayes un contenedor real que contiene uno o más valores de type T.)
  • 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_viewe incluso antes como array_ref.

Esas partes en negrita son fundamentales para la comprensión, ¡así que no las pierda ni las malinterprete! A spanNO es una matriz C de estructuras, ni es una estructura de una matriz C de tipo Tmá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 Tmá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 Tapunta el puntero para escribir. De esta manera, la única sobrecarga que ha agregado al usar a spanson las variables para almacenar el puntero y la longitud, y cualquier función de acceso conveniente que utilice y que spanproporcione.

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::vectores básicamente solo a std::arrayque también crece dinámicamente (generalmente duplica su tamaño) cada vez que se llena y tratas de agregarle algo más. . A std::arraytiene un tamaño fijo y ni spansiquiera 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::spanes 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:

  1. std::span<T, Extent>( Extent= "el número de elementos en la secuencia, o std::dynamic_extentsi es dinámico". Un intervalo simplemente apunta a la memoria y facilita el acceso, ¡pero NO la administra!):
  2. https://en.cppreference.com/w/cpp/container/span
  3. std::array<T, N>(¡fíjate que tiene un tamaño fijoN !):
  4. https://en.cppreference.com/w/cpp/container/array
  5. http://www.cplusplus.com/reference/array/array/
  6. std::vector<T>(crece de tamaño automáticamente y dinámicamente según sea necesario):
  7. https://en.cppreference.com/w/cpp/container/vector
  8. http://www.cplusplus.com/reference/vector/vector/

¿ Cómo puedo utilizar spanC++ 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:

  1. Sitio principal: https://abseil.io/
  2. https://abseil.io/docs/cpp/
  3. Repositorio de GitHub: https://github.com/abseil/abseil-cpp
  4. span.hencabezado y absl::Span<T>(array, length)clase de plantilla: https://github.com/abseil/abseil-cpp/blob/master/absl/types/span.h#L153

Otras referencias:

  1. Estructura con variables de plantilla en C++
  2. Wikipedia: clases de C++
  3. visibilidad predeterminada de los miembros de clase/estructura de C++

Relacionado:

  1. [otra de mis respuestas sobre plantillas y tramos] Cómo hacer tramos de tramos
Gabriel Staples avatar Apr 14 '2020 20:04 Gabriel Staples

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.

Tolar avatar Jun 30 '2022 12:06 Tolar