bit galleggiante e rigorosa aliasing
-
29-09-2019 - |
Domanda
Sto cercando di estrarre i bit da un galleggiante senza invocare un comportamento indefinito. Qui è il mio primo tentativo:
unsigned foo(float x)
{
unsigned* u = (unsigned*)&x;
return *u;
}
Se ho capito bene, questo non è garantito il funzionamento a causa di rigide regole di aliasing, giusto? Funziona se un fare un passo intermedio con un puntatore a carattere?
unsigned bar(float x)
{
char* c = (char*)&x;
unsigned* u = (unsigned*)c;
return *u;
}
O devo estrarre i singoli byte solo?
unsigned baz(float x)
{
unsigned char* c = (unsigned char*)&x;
return c[0] | c[1] << 8 | c[2] << 16 | c[3] << 24;
}
Naturalmente questo ha lo svantaggio di seconda endianness, ma ho potuto vivere con questo.
L'hack unione è sicuramente un comportamento indefinito, giusto?
unsigned uni(float x)
{
union { float f; unsigned u; };
f = x;
return u;
}
Solo per completezza, ecco un versione di riferimento di foo
. Inoltre comportamento indefinito, giusto?
unsigned ref(float x)
{
return (unsigned&)x;
}
Quindi, è possibile estrarre i bit da un galleggiante ( assumendo entrambi sono 32 bit di larghezza , ovviamente)? ??
EDIT: E qui è la versione memcpy
come proposto dal Goz. Dal momento che molti compilatori non supportano static_assert
ancora, ho sostituito static_assert
con alcuni template metaprogrammazione:
template <bool, typename T>
struct requirement;
template <typename T>
struct requirement<true, T>
{
typedef T type;
};
unsigned bits(float x)
{
requirement<sizeof(unsigned)==sizeof(float), unsigned>::type u;
memcpy(&u, &x, sizeof u);
return u;
}
Soluzione
L'unico modo per evitare eventuali problemi veramente è quello di memcpy.
unsigned int FloatToInt( float f )
{
static_assert( sizeof( float ) == sizeof( unsigned int ), "Sizes must match" );
unsigned int ret;
memcpy( &ret, &f, sizeof( float ) );
return ret;
}
Poiché si sta memcpying un importo fisso il compilatore di ottimizzare fuori.
Detto questo il metodo di unione è molto ampiamente supportato.
Altri suggerimenti
L'hack unione è sicuramente un comportamento indefinito, giusto?
Sì e no. Secondo lo standard, è sicuramente un comportamento indefinito. Ma è un tale trucco comunemente usato che GCC e MSVC e per quanto ne so, ogni altro compilatore popolare, garantisce esplicitamente che è sicuro e funziona come previsto.
Di seguito non violare la regola aliasing, perché non ha alcun uso di lvalue di accesso a diversi tipi ovunque
template<typename B, typename A>
B noalias_cast(A a) {
union N {
A a;
B b;
N(A a):a(a) { }
};
return N(a).b;
}
unsigned bar(float x) {
return noalias_cast<unsigned>(x);
}
Se si vuole veramente essere agnostici circa la dimensione del tipo galleggiante e solo restituire i pezzi grezzi, fare qualcosa di simile:
void float_to_bytes(char *buffer, float f) {
union {
float x;
char b[sizeof(float)];
};
x = f;
memcpy(buffer, b, sizeof(float));
}
Quindi chiamare in questo modo:
float a = 12345.6789;
char buffer[sizeof(float)];
float_to_bytes(buffer, a);
Questa tecnica, naturalmente, prodotti potenza specifica di ordinamento dei byte della macchina.