¿Está std::memcpy entre diferentes tipos copiables trivialmente un comportamiento indefinido?

9 minutos de lectura

avatar de usuario
gueza

he estado usando std::memcpy evitar alias estricto por mucho tiempo.

Por ejemplo, inspeccionar un floatcomo esto:

float f = ...;
uint32_t i;
static_assert(sizeof(f)==sizeof(i));
std::memcpy(&i, &f, sizeof(i));
// use i to extract f's sign, exponent & significand

Sin embargo, esta vez, revisé el estándar, no encontré nada que valide esto. Todo lo que encontré es este:

Para cualquier objeto (que no sea un subobjeto potencialmente superpuesto) de tipo T copiable trivialmente, ya sea que el objeto tenga o no un valor válido de tipo T, los bytes subyacentes ([intro.memory]) que forman el objeto se pueden copiar en una matriz de char, char sin firmar o std​::​byte ([cstddef.syn]).40 Si el contenido de esa matriz se vuelve a copiar en el objeto, el objeto conservará posteriormente su valor original.[Ejemplo:[ Example:

#define N sizeof(T)
char buf[N];
T obj;                          // obj initialized to its original value
std::memcpy(buf, &obj, N);      // between these two calls to std​::​memcpy, obj might be modified
std::memcpy(&obj, buf, N);      // at this point, each subobject of obj of scalar type holds its original value

— fin del ejemplo]

y este:

Para cualquier tipo T copiable trivialmente, si dos punteros a T apuntan a objetos T distintos obj1 y obj2, donde ni obj1 ni obj2 son un subobjeto potencialmente superpuesto, si los bytes subyacentes ([intro.memory]) que forman obj1 se copian en obj2,41 obj2 tendrá posteriormente el mismo valor que obj1. [ Example:

T* t1p;
T* t2p;
// provided that t2p points to an initialized object ...
std::memcpy(t1p, t2p, sizeof(T));
// at this point, every subobject of trivially copyable type in *t1p contains
// the same value as the corresponding subobject in *t2p

— end example ]

Asi que, std::memcpying un float hacia/desde char[] está permitido, y std::memcpyTambién se permite intercambiar entre los mismos tipos triviales.

¿Está bien definido mi primer ejemplo (y la respuesta vinculada)? O la forma correcta de inspeccionar un float Es para std::memcpy en un unsigned char[] tampón y usando shiftarena ors para construir un uint32_t ¿de eso?


Nota: mirando std::memcpyLas garantías de pueden no responder a esta pregunta. Hasta donde yo sé, podría reemplazar std::memcpy con un simple ciclo de copia de bytes, y la pregunta será la misma.

  • Mientras sean del mismo tamaño no debería haber problema. Sin embargo, si sólo necesita interpretar f como uint32_t solo puedes escribir (uint32_t&)f. Interpretará la ubicación de la memoria del float como si fuera uint32_t.

    – SIN NOMBRE

    12 de julio de 2018 a las 8:27

  • @NO_NAME Mi experimento muestra que su sugerencia infringe las estrictas reglas de creación de alias. coliru.stacked-crooked.com/a/bb54317049f5c8fc

    usuario2486888

    12 de julio de 2018 a las 8:35


  • Relacionado: stackoverflow.com/questions/3275353/c-aliasing-rules-and-memcpy

    – xskxzr

    12 de julio de 2018 a las 8:50

  • Relacionado: stackoverflow.com/questions/17789928/…

    – plasmacel

    12 de julio de 2018 a las 9:00

  • @NO_NAME Todavía viola las estrictas reglas de alias. La sintaxis válida no implica una operación válida. Al igual que la oración en inglés “Ideas verdes incoloras duermen furiosamente” es gramaticalmente correcto pero sin sentido.

    usuario2486888

    12 de julio de 2018 a las 9:01


Es posible que el estándar no diga correctamente que esto está permitido, pero es casi seguro que se supone que lo está y, según mi leal saber y entender, todas las implementaciones tratarán esto como un comportamiento definido.

Para facilitar la copia en un formato real char[N] objeto, los bytes que componen el f Se puede acceder al objeto como si fuera un char[N]. Esta parte, creo, no está en disputa.

bytes de un char[N] que representan un uint32_t el valor se puede copiar en un uint32_t objeto. Esta parte, creo, tampoco está en disputa.

Igualmente indiscutible, creo, es que, por ejemplo, fwrite puede haber escrito los bytes en una ejecución del programa, y fread puede haberlos vuelto a leer en otra ejecución, o incluso en otro programa por completo.

Debido a esa última parte, creo que no importa de dónde provengan los bytes, siempre que formen una representación válida de algunos uint32_t objeto. Tú pudo han recorrido todo float valores, usando memcmp en cada uno hasta que obtuviste la representación que querías, que sabías que sería idéntica a la del uint32_t valor que está interpretando como. Tú pudo incluso haber hecho eso en otro programa, un programa que el compilador nunca ha visto. Eso hubiera sido válido.

Si desde la perspectiva de la implementación, su código es indistinguible de un código inequívocamente válido, su código debe verse como válido.

  • Separar los pasos individuales involucrados y calificar cada uno como indiscutible es aclarar las cosas.

    – Peter – Reincorporar a Mónica

    12 de julio de 2018 a las 9:25

  • Sin embargo, lo que observó OP es interesante: mientras que el estándar en 6.9.2 permite explícitamente copiar bytes fuera de un objeto trivialmente copiable del que carece (o parece carecer, solo miré todas las apariciones de memcpy en n4659) una regla explícita que permite copiar bytes dentro tal objeto. Probablemente se considere entendido por sí mismo; el ejemplo en 6.9.2 copia los bytes, después de todo.

    – Peter – Reincorporar a Mónica

    12 de julio de 2018 a las 9:42

  • @PeterA.Schneider Correcto. Hay “Si el contenido de esa matriz se vuelve a copiar en el objeto, el objeto mantendrá posteriormente su valor original”. que otorga permiso para copiar de nuevo en un objeto copiable trivialmente, pero el permiso general para copiar en (en lugar de volver a) un objeto copiable trivialmente nunca se otorga explícitamente en el estándar, solo se puede inferir. Esa es la esencia de mi respuesta.

    usuario743382

    12 de julio de 2018 a las 10:39

  • ¡Buen razonamiento! Aunque tengo una pregunta. A float -> char[] la copia está bien. A char[] -> uint32_t está bien también. Pero, es un directo float -> uint32_t ¿Bien también?

    – geza

    12 de julio de 2018 a las 12:17

  • @geza Es dudoso, pero yo diría que desde el tratamiento de los bytes en ese float como un char[] está permitido, cuando haces un directo float -> uint32_ten cierto sentido, usted son copiando de un char[] a un uint32_t.

    usuario743382

    12 de julio de 2018 a las 12:26

avatar de usuario
erorika

¿Está bien definido mi primer ejemplo (y la respuesta vinculada)?

El comportamiento no está indefinido (a menos que el tipo de destino tenga representaciones de captura que no son compartidos por el tipo de origen), pero el valor resultante del entero está definido por la implementación. El estándar no garantiza cómo se representan los números de punto flotante, por lo que no hay forma de extraer la mantisa, etc., del entero de forma portátil; dicho esto, limitarse al uso de sistemas IEEE 754 no lo limita mucho en estos días.

Problemas de portabilidad:

  • IEEE 754 no está garantizado por C++
  • No se garantiza que el byte endian de float coincida con el entero endian.
  • (Sistemas con representaciones trampa).

Puedes usar std::numeric_limits::is_iec559 para verificar si su suposición sobre la representación es correcta.

Aunque, parece que uint32_t no puede tener trampas (ver comentarios) por lo que no debe preocuparse. Mediante el uso uint32_tya ha descartado la portabilidad a sistemas esotéricos: no se requieren sistemas conformes con el estándar para definir ese alias.

  • @StoryTeller hasta donde yo sé, unsigned char es el único tipo garantizado para no tener trampas.

    – erorika

    12 de julio de 2018 a las 9:33

  • @StoryTeller correcto, por lo que los alias de tipos de ancho exacto tienen garantías (más allá de los tipos para los que son un alias, si son alias estándar int o tal). Eso es un poco extraño, pero siempre es bueno poder ignorar las trampas, así que lo aceptaré 🙂

    – erorika

    12 de julio de 2018 a las 9:53

  • “El comportamiento no está indefinido”. ¿Por qué? ¿Hay algo en el estándar que lo defina?

    – geza

    12 de julio de 2018 a las 10:06

  • Sí, dan un ejemplo de lo que han escrito previamente en el texto normativo. Podría copiar bytes con un simple ciclo de copia de bytes, el resultado será el mismo. Entonces el punto no es memcpy aquí, pero el principio. ¿Puedo copiar un float a uint32_t byte a byte por algún medio? ¿Está definido por la norma?

    – geza

    12 de julio de 2018 a las 10:55

  • Muchas implementaciones representan ciertos tipos de datos de manera diferente en los registros y en la memoria. Es común en plataformas RISC de 32 bits, por ejemplo, que un uint16_t que se coloca en un registro tendrá 16 bits de datos y 16 bits de relleno que se ponen a cero cuando se escribe el valor. Si dicho objeto se lee cuando no está inicializado, puede generar un valor que a veces se comporta como si estuviera fuera del rango 0-65535 pero a veces se comporta como si estuviera dentro de ese rango. Tal comportamiento se remonta a décadas antes de Itanium.

    – Super gato

    12 de julio de 2018 a las 20:24

avatar de usuario
Gallo con sombrero

Su ejemplo está bien definido y no rompe el alias estricto. std::memcpy dice claramente:

copias count bytes del objeto apuntado por src al objeto apuntado por dest. Ambos objetos se reinterpretan como matrices de
unsigned char.

El estándar permite aliasing de cualquier tipo a través de un (signed/unsigned) char* o std::byte y por lo tanto su ejemplo no exhibe UB. Sin embargo, si el número entero resultante tiene algún valor, es otra cuestión.


use i to extract f's sign, exponent & significand

Sin embargo, esto no está garantizado por la norma ya que el valor de un float está definido por la implementación (aunque en el caso de IEEE 754 funcionará).

  • Si podemos deducir esto de la descripción de memcpyentonces, ¿por qué el estándar destaca los dos casos que mencioné en mi pregunta?

    – geza

    12 de julio de 2018 a las 10:04

  • @geza C++ se basará en C para la definición de memcpy y, como dije aquí, dice que copia n bytes “sin condición” entre dos objetos.

    – Shafik Yaghmour

    12 de julio de 2018 a las 13:08

  • Nota bit_cast utiliza memcpy como el mecanismo de juego de palabras subyacente.

    – Shafik Yaghmour

    12 de julio de 2018 a las 13:11

  • @ShafikYaghmour: sí, pero el problema no está ahí. Por ejemplo, si usted memcpy un tipo copiable no trivial, es UB, porque el estándar no lo permite. Al igual que no permite mi ejemplo explícitamente.

    – geza

    12 de julio de 2018 a las 13:15

  • @ShafikYaghmour: no es un problema. Una biblioteca proporcionada por el compilador puede usar cualquier UB que desee, vea mi comentario debajo de la pregunta.

    – geza

    12 de julio de 2018 a las 13:15

¿Ha sido útil esta solución?