bitwise casting uint32_t to float in C/C++
Asked Answered
S

3

12

I'm receiving a buffer from a network which was converted to an array of 32-bit words. I have one word which is defined as an IEEE-754 float by my interface document. I need to extract this word from the buffer. It's tough to cast from one type to another without invoking a conversion. The bits are already adhere to the IEEE-754 float standard, I don't want to re-arrange any bits.

My first try was to cast the address of the uint32_t to a void*, then convert the void* to a float*, then dereference as a float:

float ieee_float(uint32_t f)
{
    return *((float*)((void*)(&f)));
}

error: dereferencing type-punned pointer will break strict-aliasing rules [-Werror=strict-aliasing]

My second try was like this:

float ieee_float(uint32_t f)
{
    union int_float{
        uint32_t i;
        float f;
    } tofloat;

    tofloat.i = f;
    return tofloat.f;
}

However, word on the street is that unions are totally unsafe. It's undefined behavior to read from the member of the union that wasn't most recently written.

So I tried a more C++ approach:

float ieee_float(uint32_t f)
{
  return *reinterpret_cast<float*>(&f);
}

error: dereferencing type-punned pointer will break strict-aliasing rules [-Werror=strict-aliasing]

My next thought was "screw it. Why am I dealing with pointers anyways?" and just tried:

float ieee_float(uint32_t f)
{
  return reinterpret_cast<float>(f);
}

error: invalid cast from type ‘uint32_t {aka unsigned int}’ to type ‘float’

Is there a way to do the conversion without triggering the warning/error? I'm compiling with g++ using -Wall -Werror. I'd prefer to not touch compiler settings.

I tagged C because a c-solution is acceptable.

Skilful answered 15/2, 2018 at 9:2 Comment(6)
If you are compiling as C++, a C solution need not work. Different languages and all that. Or are you gonna compile that one function by a C compiler and link it in?Marx
If a C-developer has a C-solution, I'd be happy to accept it because it'd probably work in C++ as well. This is more a question of syntax than anything.Skilful
There's no way to do this without 'breaking the rules' or writing unsafe code. The union approach seems best to me.Collision
Rubbish. Punning via union is the C solution. It's not undefined behavior, and will do exactly what you want so long as you don't end up with a trap representation. And yet in C++ it's undefined behavior. See the problem with double tagging?Marx
What should happen when sizeof(float) != sizeof(std::uint32_t)?Orme
float ieee_float(uint32_t f) { void *p = &f; float fv = *(float*)p; return fv; } but the union is cleaner typedef union { float f; uint32_t v; } fu; then float ieee_float(uint32_t f) { fu.v = f; return fu.f; }Jezreel
D
15

In C++20, you can use std::bit_cast:

float ieee_float(uint32_t f)
{
    return std::bit_cast<float>(f);
}

In C++17 and before, the right way™ is:

float ieee_float(uint32_t f)
{
    static_assert(sizeof(float) == sizeof f, "`float` has a weird size.");
    float ret;
    std::memcpy(&ret, &f, sizeof(float));
    return ret;
}

Both GCC and Clang at -O1 and above generate the same assembly for this code and a naive reinterpret_cast<float &>(f) (but the latter is undefined behavior, and might not work in some scenarios).

Diarmuid answered 15/2, 2018 at 9:17 Comment(12)
@Orme - All versions do a copy eventually. It returns by value after all.Marx
Instead of using 4, I would use sizeof(uint32_t).Heavyset
It's dangerous to go alone! Take this: [basic.val]/11.8.Broke
@Heavyset Or sizeof f.Orme
In reinterpret_cast<float &>(f) please change the & to *Simaroubaceous
@Simaroubaceous This is not a typo. You can write reinterpret_cast<T &>(x) as a shorthand for *reinterpret_cast<T *>(&x).Diarmuid
@Diarmuid I just checked this in cpp.sh and you are right. How long has this been around ? I really hate C++ because of all the hidden stuff.Simaroubaceous
@Simaroubaceous I think it was always there.Diarmuid
@Simaroubaceous It is 6) in en.cppreference.com/w/cpp/language/reinterpret_castDecant
@Decant in 6) they don't talk about reinterpret_cast<T &> or I didn't understand the gibberish about lvalue.Simaroubaceous
@Simaroubaceous The whole page is about reinterpret_cast and 6) is about converting to reference to T2, which in total means reinterpret_cast<T2&>(lvalue) with lvalue of type T1.Decant
for my own FYI godbolt.org/z/h3nxPbWTGJainism
U
2

There's no C/C++ language. They're different languages with different rules. The valid way in C is to use a union, but that's not allowed in C++. See

In older C++ standards you have to use std::memcpy. Even reinterpret_cast for type punning invokes undefined behavior, hence disallowed. In C++20 a new cast type called std::bit_cast was created exactly for this purpose

float ieee_float(uint32_t f)
{
  return std::bit_cast<float>(f);
}

See also:

Unasked answered 26/1, 2022 at 16:52 Comment(3)
Some casts are allowed according to type casting rules.Decant
@Decant which cast are you talking about? Nothing other than bit_cast is allowed to do type punningUnasked
See type aliasing section: en.cppreference.com/w/cpp/language/reinterpret_cast Cast to std::byte, char and unsigned char (but not signed char, if not the same as char depending on platform) is allowed.Decant
C
0

You have several options, as stated here:

  • Use the union solution: since C11 it is explicitly allowed (as said in the other answer).
  • Instead of using an array of 32 bit words, use an array of 8 bit words (uint8_t), since char types can be aliased to any type.
Capitalization answered 15/2, 2018 at 9:11 Comment(5)
The question was retagged to pure C++, so (1) doesn't apply. Also I highly doubt the (2) is well-defined...Diarmuid
IRC, any type can be aliased as unsigned char.Broke
@Broke Yes, but IIRC not the other way around.Diarmuid
@Diarmuid that's it.Broke
Using union for type punning is also allowed in C99. And even in C90 it was allowed if implementation supported it.Heavyset

© 2022 - 2024 — McMap. All rights reserved.