27

Note: I mistakenly asked about static_cast originally; this is why the top answer mentions static_cast at first.

I have some binary files with little endian float values. I want to read them in a machine-independent manner. My byte-swapping routines (from SDL) operate on unsigned integers types.

Is it safe to simply cast between ints and floats?

float read_float() {
    // Read in 4 bytes.
    Uint32 val;
    fread( &val, 4, 1, fp );
    // Swap the bytes to little-endian if necessary.
    val = SDL_SwapLE32(val);
    // Return as a float
    return reinterpret_cast<float &>( val );  //XXX Is this safe?
}

I want this software to be as portable as possible.

Daniel Hanrahan
  • 4,801
  • 7
  • 31
  • 44

3 Answers3

37

Well, static_cast is "safe" and it has defined behavior, but this is probably not what you need. Converting an integral value to float type will simply attempt to represent the same integral value in the target floating-point type. I.e. 5 of type int will turn into 5.0 of type float (assuming it is representable precisely).

What you seem to be doing is building the object representation of float value in a piece of memory declared as Uint32 variable. To produce the resultant float value you need to reinterpret that memory. This would be achieved by reinterpret_cast

assert(sizeof(float) == sizeof val);
return reinterpret_cast<float &>( val );

or, if you prefer, a pointer version of the same thing

assert(sizeof(float) == sizeof val);
return *reinterpret_cast<float *>( &val );

Although this sort of type-punning is not guaranteed to work in a compiler that follows strict-aliasing semantics. Another approach would be to do this

float f;

assert(sizeof f == sizeof val);
memcpy(&f, &val, sizeof f);

return f;

Or you might be able to use the well-known union hack to implement memory reinterpretation. This is formally illegal in C++ (undefined behavior), meaning that this method can only be used with certain implementations that support it as an extension

assert(sizeof(float) == sizeof(Uint32));

union {
  Uint32 val; 
  float f;
} u = { val };

return u.f;
AnT stands with Russia
  • 312,472
  • 42
  • 525
  • 765
  • 1
    Apologies, you are correct (I meant reinterpret_cast, not static_cast). I've updated the question to reflect this. – Daniel Hanrahan Dec 20 '12 at 23:59
  • Thanks. I wasn't aware of the term "type-punning". It has turned up some useful info. Based on what I've read, I think I'll go with the *union* trick, it seems to be well-supported. – Daniel Hanrahan Dec 21 '12 at 00:11
  • What if we cast to void * first and then float * to get past of multiplication of 4? Is it safe? – huseyin tugrul buyukisik Sep 18 '16 at 21:26
  • 1
    union is UB for type punning. use memcpy, it will elide and should not be expressed in the compiler output – Beached Feb 08 '19 at 02:14
  • 1
    by "a compiler that follows strict-aliasing semantics." you mean a conforming compiler – M.M Feb 08 '19 at 02:42
  • @M.M: I don't see how "following" or "not following" strict-aliasing semantics can be used to describe conformance properties of the compiler. The same can be said to any language feature related to undefined behavior (as long as the compiler makes sure that behavior is defined where it supposed to be defined). – AnT stands with Russia Feb 08 '19 at 02:56
3

In short, it's incorrect. You are casting an integer to a float, and it will be interpreted by the compiler as an integer at the time. The union solution presented above works.

Another way to do the same sort of thing as the union is would be to use this:

return *reinterpret_cast<float*>( &val );

It is equally safe/unsafe as the union solution above, and I would definitely recommend an assert to make sure float is the same size as int.

I would also warn that there ARE floating point formats that are not IEEE-754 or IEEE-854 compatible (these two standards have the same format for float numbers, I'm not entirely sure what the detail difference is, to be honest). So, if you have a computer that uses a different floating point format, it would fall over. I'm not sure if there is any way to check that, aside from perhaps having a canned set of bytes stored away somewhere, along with the expected values in float, then convert the values and see if it comes up "right".

Mats Petersson
  • 126,704
  • 14
  • 140
  • 227
  • Well I've been reading the wiki article on type-punning now. It says: "On many common platforms, the use of pointer punning can create problems if different pointers are aligned in machine-specific ways ... This aliasing problem can be fixed by the use of a union". – Daniel Hanrahan Dec 21 '12 at 00:18
  • If the data types have different alignment, you may well have problems with them living in a union too, because it's not guaranteed that the data read from the integer overlaps with the float. But I guess we could have a compiler that thinks it's OK to put an integer at address X with an alignment of 2, and then have floats aligned to 4 bytes, in which case it would crash or behave badly) The BEST (most portable) solution is probably to store the floating point data as text, or as fixed point in integer format. That way, there's no doubt what it means, or about alignment. – Mats Petersson Dec 21 '12 at 00:24
  • That makes sense, but I think I'll stick with the union since it seems to be the best option for binary. Unfortunately, I can't use text since I'm supporting files from a legacy system. – Daniel Hanrahan Dec 21 '12 at 00:29
  • 2
    This is undefined behaviour due to strict aliasing violation – M.M Feb 08 '19 at 02:41
0

(As others have said, a reinterpret cast, where the underlying memory is treated as though it's another type, is undefined behaviour because it's up to the C++ implementation how the float is sized/aligned/placed in memory.)

Here's a templated implementation of AnT's memcpy solution, which avoids -Wstrict-aliasing warnings.

I guess this supports implementations where the sizes aren't standard, but still match one of the templated sizes - and then fails to compile if there are no matches.

(compiling with -fstrict-aliasing -Wall that actually enables -Wstrict-aliasing)

#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <type_traits>

template<size_t S> struct sized_uint;
template<> struct sized_uint<sizeof(uint8_t)> { using type = uint8_t; };
template<> struct sized_uint<sizeof(uint16_t)> { using type = uint16_t; };
template<> struct sized_uint<sizeof(uint32_t)> { using type = uint32_t; };
template<> struct sized_uint<sizeof(uint64_t)> { using type = uint64_t; };
template<size_t S> using sized_uint_t = typename sized_uint<S>::type;

template<class T> sized_uint_t<sizeof(T)> bytesAsUint(T x)
{
    sized_uint_t<sizeof(T)> result;
    // template forces size to match. memcpy handles alignment
    memcpy(&result, &x, sizeof(x));
    return result;
}

template<size_t S> struct sized_float;
template<> struct sized_float<sizeof(float)> { using type = float; };
template<> struct sized_float<sizeof(double)> { using type = double; };
template<size_t S> using sized_float_t = typename sized_float<S>::type;

template<class T> sized_float_t<sizeof(T)> bytesAsFloat(T x)
{
    sized_float_t<sizeof(T)> result;
    memcpy(&result, &x, sizeof(x));
    return result;
}

// Alt for just 'float'
//template<class T> std::enable_if_t<sizeof(T) == sizeof(float), float> bytesAsFloat(T x)
//{
//    float result;
//    memcpy(&result, &x, sizeof(x));
//    return result;
//}

float readIntAsFloat(uint32_t i)
{
    // error: no matching function for call to 'bytesAsFloat(uint16_t)'
    //return bytesAsFloat((uint16_t)i);

    return bytesAsFloat(i);
}

void printFloat(float f) {
    // warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
    //printf("Float %f: %x", f, reinterpret_cast<unsigned int&>(f));

    printf("Float %f: %x", f, bytesAsUint(f));
}

(godbolt)

jozxyqk
  • 16,424
  • 12
  • 91
  • 180