¿Por qué convertir un puntero a un flotador en un puntero a un largo y luego eliminar la referencia?

9 minutos de lectura

avatar de usuario de bobbay
bobbay

Estaba revisando este ejemplo que tiene una función que genera un patrón de bits hexadecimal para representar un flotador arbitrario.

void ExamineFloat(float fValue)
{
    printf("%08lx\n", *(unsigned long *)&fValue);
}

¿Por qué tomar la dirección de fValue, convertirla en un puntero largo sin firmar y luego eliminar la referencia? ¿No es todo ese trabajo equivalente a un elenco directo para unsigned long?

printf("%08lx\n", (unsigned long)fValue);

Lo probé y la respuesta no es la misma, tan confundido.

  • Este es un comportamiento indefinido. Es algo que la gente hacía antes de que se estandarizara C en 1989, y algunos no se han mantenido al día.

    –MM

    3 sep 2016 a las 23:11

  • Esto probablemente ni siquiera funcione en arquitecturas de 64 bits (LP64)…

    – usuario3840170

    18 dic a las 15:13


Avatar de usuario de Daniel Jour
daniel jour

(unsigned long)fValue

Esto convierte el float valor a un unsigned long valor, de acuerdo con las “conversiones aritméticas habituales”.

*(unsigned long *)&fValue

La intención aquí es tomar la dirección en la que fValue está almacenado, pretenda que no hay un float pero un unsigned long en esta dirección, y luego leer que unsigned long. El propósito es examinar el patrón de bits que se utiliza para almacenar el float en memoria.

Sin embargo, como se muestra, esto provoca un comportamiento indefinido.

Motivo: no puede acceder a un objeto a través de un puntero a un tipo que no es “compatible” con el tipo del objeto. Los tipos “compatibles” son, por ejemplo, (unsigned) char y cualquier otro tipo, o estructuras que comparten los mismos miembros iniciales (hablando de C aquí). Ver §6.5/7 N1570 para la lista detallada (C11) (Tenga en cuenta que mi uso de “compatible” es diferente, más amplio, que en el texto de referencia.)

Solución: Transmitir a unsigned char *acceda a los bytes individuales del objeto y ensamble un unsigned long fuera de ellos:

unsigned long pattern = 0;
unsigned char * access = (unsigned char *)&fValue;
for (size_t i = 0; i < sizeof(float); ++i) {
  pattern |= *access;
  pattern <<= CHAR_BIT;
  ++access;
}

Tenga en cuenta que (como señaló @CodesInChaos) lo anterior trata el valor de punto flotante como almacenado con su byte más significativo primero (“big endian”). Si su sistema usa un orden de bytes diferente para los valores de coma flotante, deberá ajustarlo (o reorganizar los bytes de arriba unsigned longlo que sea más práctico para usted).

  • haría reinterpret_cast<unsigned long&>(fValue) estar permitido/definido en C++ (suponiendo que los tamaños de letra se ajusten, por supuesto)?

    – celtschk

    4 de septiembre de 2016 a las 5:28


  • El código original funciona siempre que el endian de flotantes y enteros sea el mismo (ignorando el UB). Su código asume big-endian. usaría un memcpy en uint32_t (y una afirmación para tamaños coincidentes).

    – CodesInChaos

    4 de septiembre de 2016 a las 8:41

  • @celtschk Me sorprendería mucho si el uso de esa referencia no contara como una violación estricta de alias. — “Una expresión lvalue de tipo T1 se puede convertir para hacer referencia a otro tipo T2. El resultado es un lvalue o xvalue que se refiere al mismo objeto que el lvalue original, pero con un tipo diferente. No se crea ningún temporal, no se copia hecho, no se llaman constructores o funciones de conversión. La referencia resultante solo se puede acceder de forma segura si lo permiten las reglas de alias de tipo ” (origen)

    – CodesInChaos

    4 de septiembre de 2016 a las 8:48


avatar de usuario de md5
md5

Los valores de punto flotante tienen representaciones de memoria: por ejemplo, los bytes pueden representar un valor de punto flotante usando IEEE754.

la primera expresión *(unsigned long *)&fValue interpretará estos bytes como si fuera el representación de un unsigned long valor. De hecho, en el estándar C da como resultado un comportamiento indefinido (de acuerdo con la llamada “regla estricta de aliasing”). En la práctica, hay cuestiones como la endianidad que deben tenerse en cuenta.

La segunda expresión (unsigned long)fValue es compatible con el estándar C. Tiene un significado preciso:

C11 (n1570), § 6.3.1.4 Real flotante y entero

Cuando un valor finito de tipo flotante real se convierte en un tipo entero distinto de _Bool, la parte fraccionaria se descarta (es decir, el valor se trunca hacia cero). Si el valor de la parte integral no puede ser representado por el tipo entero, el comportamiento es indefinido.

*(unsigned long *)&fValue no equivale a un reparto directo a un unsigned long.

la conversión a (unsigned long)fValue convierte el valor de fValue en una unsigned longusando las reglas normales para la conversión de un float valor a un unsigned long valor. La representación de ese valor en un unsigned long (por ejemplo, en términos de bits) puede ser bastante diferente de cómo se representa ese mismo valor en un float.

La conversión *(unsigned long *)&fValue formalmente tiene un comportamiento indefinido. Interpreta la memoria ocupada por fValue como si fuera un unsigned long. En la práctica (es decir, esto es lo que sucede a menudo, aunque el comportamiento no está definido), esto a menudo arrojará un valor bastante diferente de fValue.

Typecasting en C hace tanto una conversión de tipo como una conversión de valor. La conversión de punto flotante → largo sin signo trunca la parte fraccionaria del número de punto flotante y restringe el valor al rango posible de un largo sin signo. La conversión de un tipo de puntero a otro no requiere un cambio en el valor, por lo que usar el tipo de puntero es una forma de mantener el mismo en memoria representación mientras cambia el tipo asociado con esa representación.

En este caso, es una forma de poder generar la representación binaria del valor de coma flotante.

Como otros ya han notado, lanzar un puntero a un tipo que no sea de caracteres a un puntero a un tipo diferente que no sea de caracteres y luego eliminar la referencia es un comportamiento indefinido.

Ese printf("%08lx\n", *(unsigned long *)&fValue) invoque un comportamiento indefinido no significa necesariamente que ejecutar un programa que intente realizar tal parodia resultará en el borrado del disco duro o hará que los demonios nasales salgan de la nariz (los dos sellos distintivos del comportamiento indefinido). En una computadora en la que sizeof(unsigned long)==sizeof(float) y en el que ambos tipos tienen los mismos requisitos de alineación, que printf Es casi seguro que hará lo que uno espera que haga, que es imprimir la representación hexadecimal del valor de punto flotante en cuestión.

Esto no debería sorprender. El estándar C invita abiertamente a implementaciones para ampliar el lenguaje. Muchas de estas extensiones se encuentran en áreas que son, en sentido estricto, de comportamiento indefinido. Por ejemplo, la función POSIX dlsim devuelve un void*, pero esta función generalmente se usa para encontrar la dirección de una función en lugar de una variable global. Esto significa que el puntero vacío devuelto por dlsym debe convertirse en un puntero de función y luego desreferenciarse para llamar a la función. Obviamente, este es un comportamiento indefinido, pero no obstante funciona en cualquier plataforma compatible con POSIX. Esto no funcionará en una máquina con arquitectura Harvard en la que los punteros a funciones tengan tamaños diferentes a los de los punteros a datos.

De manera similar, lanzar un puntero a un float a un puntero a un entero sin signo y luego la desreferenciación funciona en casi cualquier computadora con casi cualquier compilador en el que los requisitos de tamaño y alineación de ese entero sin signo son los mismos que los de un float.

Dicho esto, usando unsigned long bien podría meterte en problemas. En mi computadora, un unsigned long tiene una longitud de 64 bits y tiene requisitos de alineación de 64 bits. Esto no es compatible con un flotador. seria mejor usar uint32_t — en mi computadora, eso es.

El truco sindical es una forma de evitar este lío:

typedef struct {
    float fval;
    uint32_t ival;
} float_uint32_t;

Asignando a un float_uint32_t.fval y acceder desde un “float_uint32_t.ival` solía ser un comportamiento indefinido. Ese ya no es el caso en C. Ningún compilador que yo sepa sopla demonios nasales para el sindicato. Esto no era UB en C++. era ilegal Hasta C ++ 11, un compilador de C ++ compatible tenía que quejarse para cumplir.

Cualquier forma aún mejor de evitar este lío es usar el %a formato, que forma parte del estándar C desde 1999:

printf ("%a\n", fValue);

Esto es simple, fácil, portátil y no hay posibilidad de un comportamiento indefinido. Esto imprime la representación hexadecimal/binaria del valor de punto flotante de doble precisión en cuestión. Desde printf es una función arcaica, todo float Los argumentos se convierten en double antes de la llamada a printf. Esta conversión debe ser exacta según la versión de 1999 del estándar C. Uno puede recoger ese valor exacto a través de una llamada a scanf o sus hermanas.

  • Gracias por agregar esta respuesta, ¡ayuda a aclarar las cosas aún más! salud.

    – bobbay

    7 sep 2016 a las 16:39

  • Re “Ese ya no es el caso en C” para el truco sindical, estoy bastante seguro de que está equivocado. C11 6.5/7 establece las circunstancias en las que se puede acceder a los valores almacenados mediante un lvalue y, dado que float y uint32_t no son compatibles, no está permitido. Las reglas sobre el acceso solo a uniones con el mismo campo utilizado en la última tienda siguen vigentes. A menos que tenga una versión posterior del estándar que cambie esto, pero dado que C17 solo se creó debido a las reglas ISO que limitan los apéndices antes de que se requiera un nuevo estándar, dudo que hayan hecho un cambio tan fundamental.

    – pax diablo

    15 de junio de 2020 a las 13:45

  • Gracias por agregar esta respuesta, ¡ayuda a aclarar las cosas aún más! salud.

    – bobbay

    7 sep 2016 a las 16:39

  • Re “Ese ya no es el caso en C” para el truco sindical, estoy bastante seguro de que está equivocado. C11 6.5/7 establece las circunstancias en las que se puede acceder a los valores almacenados mediante un lvalue y, dado que float y uint32_t no son compatibles, no está permitido. Las reglas sobre el acceso solo a uniones con el mismo campo utilizado en la última tienda siguen vigentes. A menos que tenga una versión posterior del estándar que cambie esto, pero dado que C17 solo se creó debido a las reglas ISO que limitan los apéndices antes de que se requiera un nuevo estándar, dudo que hayan hecho un cambio tan fundamental.

    – pax diablo

    15 de junio de 2020 a las 13:45

¿Ha sido útil esta solución?