std :: bit_cast con std :: array

14

En su reciente charla "Punking de tipo en C ++ moderno", Timur Doumler dijo que std::bit_castno se puede usar para convertir un bit floaten un unsigned char[4]porque las matrices de estilo C no se pueden devolver de una función. Deberíamos usar std::memcpyo esperar hasta C ++ 23 (o posterior) cuando algo así reinterpret_cast<unsigned char*>(&f)[i]esté bien definido.

En C ++ 20, ¿podemos usar un std::arraycon std::bit_cast,

float f = /* some value */;
auto bits = std::bit_cast<std::array<unsigned char, sizeof(float)>>(f);

en lugar de una matriz de estilo C para obtener bytes de a float?

Evg
fuente

Respuestas:

15

Sí, esto funciona en todos los compiladores principales, y por lo que puedo ver al observar el estándar, es portátil y garantiza que funcione.

En primer lugar, std::array<unsigned char, sizeof(float)>se garantiza que sea un agregado ( https://eel.is/c++draft/array#overview-2 ). De esto se deduce que contiene exactamente un sizeof(float)número de chars en el interior (generalmente como un char[], aunque es unfa que el estándar no exige esta implementación en particular, pero sí dice que los elementos deben ser contiguos) y no puede tener ningún miembro no estático adicional.

Por lo tanto, es trivialmente copiable, y su tamaño coincide con el de floattambién.

Esas dos propiedades te permiten bit_castentre ellas.

Timur Doumler
fuente
3
Tenga en cuenta que struct X { unsigned char elems[5]; };satisface la regla que está citando. Ciertamente se puede inicializar en una lista con hasta 4 elementos. Puede también ser inicializado lista con los 5 elementos. No creo que ningún implementador de biblioteca estándar odie a las personas lo suficiente como para hacer esto, pero creo que es técnicamente conforme.
Barry
¡Gracias! - Barry, no creo que sea del todo correcto. El estándar dice: "se puede inicializar en una lista con hasta N elementos". Mi interpretación es que "hasta" implica "no más que". Lo que significa que no puedes hacer elems[5]. Y en ese punto no puedo ver cómo podrías terminar con un agregado sizeof(array<char, sizeof(T)>) != sizeof(T)¿ dónde ?
Timur Doumler
Creo que el propósito de la regla ("un agregado que se puede inicializar en una lista ...") es permitir cualquiera de ellos struct X { unsigned char c1, c2, c3, c4; };o struct X { unsigned char elems[4]; };, por lo tanto, aunque los caracteres deben ser los elementos de ese agregado, esto les permite ser elementos agregados directos o elementos de un solo sub-agregado.
Timur Doumler
2
@Timur "hasta" no implica "no más que". De la misma manera que la implicación P -> Qno implica nada sobre el caso donde!P
Barry
1
Incluso si el agregado contiene nada más que una matriz de exactamente 4 elementos, no hay garantía de que arrayno tenga relleno. Las implementaciones de este pueden no tener relleno (y cualquier implementación que lo haga debe considerarse disfuncional), pero no hay garantía de que en arraysí no lo tenga.
Nicol Bolas
6

La respuesta aceptada es incorrecta porque no tiene en cuenta los problemas de alineación y relleno.

Por [matriz] / 1-3 :

El encabezado <array>define una plantilla de clase para almacenar secuencias de objetos de tamaño fijo. Una matriz es un contenedor contiguo. Una instancia de array<T, N>almacena Nelementos de tipo T, por lo que size() == Nes una invariante.

Una matriz es un agregado que se puede inicializar en una lista con hasta N elementos cuyos tipos son convertibles T.

Una matriz cumple con todos los requisitos de un contenedor y de un contenedor reversible ( [container.requirements]), excepto que un objeto de matriz construido por defecto no está vacío y ese intercambio no tiene una complejidad constante. Una matriz cumple con algunos de los requisitos de un contenedor de secuencia. Las descripciones se proporcionan aquí solo para operaciones en una matriz que no se describen en una de estas tablas y para operaciones donde hay información semántica adicional.

El estándar en realidad no requiere std::arraytener exactamente un miembro de datos público de tipo T[N], por lo que en teoría es posible quesizeof(To) != sizeof(From) o is_­trivially_­copyable_­v<To>.

Sin embargo, me sorprendería si esto no funciona en la práctica.

LF
fuente
2

Si.

Según el documento que describe el comportamiento std::bit_casty la implementación propuesta en la medida en que ambos tipos tengan el mismo tamaño y se puedan copiar trivialmente, el reparto debe ser exitoso.

Una implementación simplificada de std::bit_castdebería ser algo como:

template <class Dest, class Source>
inline Dest bit_cast(Source const &source) {
    static_assert(sizeof(Dest) == sizeof(Source));
    static_assert(std::is_trivially_copyable<Dest>::value);
    static_assert(std::is_trivially_copyable<Source>::value);

    Dest dest;
    std::memcpy(&dest, &source, sizeof(dest));
    return dest;
}

Desde un flotante (4 bytes) y una matriz de unsigned charconsize_of(float) respecto a todas esas afirmaciones, se realizará el subyacente std::memcpy. Por lo tanto, cada elemento en la matriz resultante será un byte consecutivo del flotante.

Para probar este comportamiento, escribí un pequeño ejemplo en Compiler Explorer que puedes probar aquí: https://godbolt.org/z/4G21zS . El float 5.0 se almacena correctamente como una matriz de bytes ( Ox40a00000) que corresponde a la representación hexadecimal de ese número flotante en Big Endian .

Manuel Gil
fuente
¿Estás seguro de que std::arrayestá garantizado que no tiene bits de relleno, etc.?
LF
1
Desafortunadamente, el simple hecho de que algún código funcione no implica que no haya UB en él. Por ejemplo, podemos escribir auto bits = reinterpret_cast<std::array<unsigned char, sizeof(float)>&>(f)y obtener exactamente la misma salida. ¿Prueba algo?
Evg
@LF según la especificación: std::arraysatisface los requisitos de ContiguiosContainer (desde C ++ 17) .
Manuel Gil
1
@ManuelGil: std::vectortambién satisface los mismos criterios y obviamente no se puede usar aquí. ¿Hay algo que requiera que std::arraymantenga los elementos dentro de la clase (en un campo), evitando que sea un puntero simple a la matriz interna? (como en vector, que también tiene un tamaño, que matriz no requiere tener en un campo)
firda
@firda El requisito agregado de std::arrayefectivamente requiere que almacene los elementos dentro, pero me preocupan los problemas de diseño.
LF