c++ undefined-behavior gcc-warning strict-aliasing type-punning

c++ - ¿Cuál es la forma correcta de tipear un flotador a un int y viceversa?



undefined-behavior gcc-warning (7)

Eche un vistazo a this para obtener más información sobre el punning de tipo y el alias estricto.

La única conversión segura de un tipo en una matriz es en una matriz char . Si desea que una dirección de datos se pueda cambiar a diferentes tipos, deberá utilizar una union

El siguiente código realiza una operación de raíz cuadrada inversa rápida mediante algunos hacks de bits. El algoritmo probablemente fue desarrollado por Silicon Graphics a principios de la década de 1990 y también apareció en Quake 3. más información

Sin embargo, recibo la siguiente advertencia del compilador de GCC C ++ : la desreferenciación del puntero con tipo de letra romperá las reglas de alias estricto

¿Debo usar static_cast , reinterpret_cast o dynamic_cast en estas situaciones?

float InverseSquareRoot(float x) { float xhalf = 0.5f*x; int32_t i = *(int32_t*)&x; i = 0x5f3759df - (i>>1); x = *(float*)&i; x = x*(1.5f - xhalf*x*x); return x; }


El único elenco que funcionará aquí es reinterpret_cast . (Y aun así, al menos un compilador se saldrá de su camino para asegurarse de que no funcionará).

Pero, ¿qué estás tratando de hacer realmente? Ciertamente hay una mejor solución, que no involucra el tipeado. Hay muy, muy pocos casos en los que el tipo punning es apropiado, y todos están en código de muy, muy bajo nivel, como la serialización o la implementación de la biblioteca estándar de C (por ejemplo, funciones como modf ). De lo contrario (y quizás incluso en la serialización), las funciones como ldexp y modf probablemente funcionarán mejor, y ciertamente serán más legibles.


El elenco invoca un comportamiento indefinido. Independientemente de la forma de lanzamiento que utilice, seguirá siendo un comportamiento indefinido. No está definido, no importa qué tipo de reparto uses.

La mayoría de los compiladores harán lo que usted espera, pero a gcc le gusta ser mezquino y probablemente asumirá que no asignó los punteros a pesar de todas las indicaciones que hizo y reordena la operación para que den un resultado extraño.

Convertir un puntero a un tipo incompatible y desreferenciarlo es un comportamiento indefinido. La única excepción es std::memcpy desde char, así que la única solución es usar std::memcpy (según la respuesta de R. Martinho Fernandes). (No estoy seguro de cuánto se define utilizando uniones; aunque tiene más posibilidades de funcionar).

Dicho esto, no debes usar el estilo C en C ++. En este caso, static_cast no compilará, ni dynamic_cast , forzarlo a usar reinterpret_cast y reinterpret_cast es una sugerencia importante de que podría estar violando reglas estrictas de alias.


Hay algunas buenas respuestas aquí que abordan el problema de tipificación de tipos.

Quiero abordar la parte de "raíz inversa cuadrada rápida". No uses este "truco" en los procesadores modernos. Cada vector principal ISA tiene una instrucción de hardware dedicada para darle una raíz cuadrada inversa rápida. Cada uno de ellos es más rápido y más preciso que este pequeño truco a menudo copiado.

Todas estas instrucciones están disponibles a través de intrínsecos, por lo que son relativamente fáciles de usar. En SSE, desea utilizar rsqrtss (intrínseco: _mm_rsqrt_ss( ) ); en NEON desea usar vrsqrte (intrínseco: vrsqrte_f32( ) ); y en AltiVec quieres usar frsqrte . La mayoría de las ISA de GPU tienen instrucciones similares. Estas estimaciones se pueden refinar usando la misma iteración de Newton, y NEON incluso tiene la instrucción vrsqrts para realizar parte del refinamiento en una sola instrucción sin necesidad de cargar constantes.


Olvídate de los lanzamientos. Utilice memcpy .

float xhalf = 0.5f*x; uint32_t i; assert(sizeof(x) == sizeof(i)); std::memcpy(&i, &x, sizeof(i)); i = 0x5f375a86 - (i>>1); std::memcpy(&x, &i, sizeof(i)); x = x*(1.5f - xhalf*x*x); return x;

El código original intenta inicializar int32_t accediendo primero al objeto float través de un puntero int32_t , que es donde se rompen las reglas. El reparto de estilo C es equivalente a un reinterpret_cast , por lo que cambiarlo a reinterpret_cast no haría mucha diferencia.

La diferencia importante cuando se usa memcpy es que los bytes se copian desde el float al int32_t , pero nunca se accede al objeto float a través de un valor int32_t , porque memcpy lleva los punteros al vacío y sus interiores son "mágicos" y no rompen el reglas de aliasing.


Actualizar

Ya no creo que esta respuesta sea correcta , debido a los comentarios que he recibido del comité. Pero quiero dejarlo para propósitos informativos. Y tengo la esperanza de que esta respuesta pueda ser corregida por el comité (si así lo desea). Es decir, no hay nada sobre el hardware subyacente que hace que esta respuesta sea incorrecta, es solo el juicio de un comité lo que lo hace así, o no.

Estoy agregando una respuesta para no refutar la respuesta aceptada, sino para aumentarla. Creo que la respuesta aceptada es correcta y eficiente (y la he votado recientemente). Sin embargo, quería demostrar otra técnica que es igual de correcta y eficiente:

float InverseSquareRoot(float x) { union { float as_float; int32_t as_int; }; float xhalf = 0.5f*x; as_float = x; as_int = 0x5f3759df - (as_int>>1); as_float = as_float*(1.5f - xhalf*as_float*as_float); return as_float; }

Usando clang ++ con optimización en -O3, compilé el código de plasmacel, el código de R. Martinho Fernandes y este código, y comparé la línea de ensamblaje por línea. Los tres eran idénticos. Esto se debe a la elección del compilador para compilarlo de esta manera. Había sido igualmente válido que el compilador produjera un código diferente y roto.


Basándome en las respuestas aquí, hice una función moderna "pseudo-cast" para facilitar la aplicación.

Versión C99 (aunque la mayoría de los compiladores lo admiten, en teoría podría ser un comportamiento indefinido en algunos)

template <typename T, typename U> inline T pseudo_cast(const U &x) { static_assert(std::is_trivially_copyable<T>::value && std::is_trivially_copyable<U>::value, "pseudo_cast can''t handle types which are not trivially copyable"); union { U from; T to; } __x = {x}; return __x.to; }

Versiones universales (basadas en la respuesta aceptada)

Tipos de reparto con el mismo tamaño:

#include <cstring> template <typename T, typename U> inline T pseudo_cast(const U &x) { static_assert(std::is_trivially_copyable<T>::value && std::is_trivially_copyable<U>::value, "pseudo_cast can''t handle types which are not trivially copyable"); static_assert(sizeof(T) == sizeof(U), "pseudo_cast can''t handle types with different size"); T to; std::memcpy(&to, &x, sizeof(T)); return to; }

Tipos de fundición con cualquier tamaño:

#include <cstring> template <typename T, typename U> inline T pseudo_cast(const U &x) { static_assert(std::is_trivially_copyable<T>::value && std::is_trivially_copyable<U>::value, "pseudo_cast can''t handle types which are not trivially copyable"); T to = T(0); std::memcpy(&to, &x, (sizeof(T) < sizeof(U)) ? sizeof(T) : sizeof(U)); return to; }

Utilízalo como:

float f = 3.14f; uint32_t u = pseudo_cast<uint32_t>(f);