32

Assume I have guarantees that float is IEEE 754 binary32. Given a bit pattern that corresponds to a valid float, stored in std::uint32_t, how does one reinterpret it as a float in a most efficient standard compliant way?

float reinterpret_as_float(std::uint32_t ui) {
   return /* apply sorcery to ui */;
}

I've got a few ways that I know/suspect/assume have some issues:

  1. Via reinterpret_cast,

    float reinterpret_as_float(std::uint32_t ui) {
        return reinterpret_cast<float&>(ui);
    }
    

    or equivalently

    float reinterpret_as_float(std::uint32_t ui) {
        return *reinterpret_cast<float*>(&ui);
    }
    

    which suffers from aliasing issues.

  2. Via union,

    float reinterpret_as_float(std::uint32_t ui) {
        union {
            std::uint32_t ui;
            float f;
        } u = {ui};
        return u.f;
    }
    

    which is not actually legal, as it is only allowed to read from most recently written to member. Yet, it seems some compilers (gcc) allow this.

  3. Via std::memcpy,

    float reinterpret_as_float(std::uint32_t ui) {
        float f;
        std::memcpy(&f, &ui, 4);
        return f;
    }
    

    which AFAIK is legal, but a function call to copy single word seems wasteful, though it might get optimized away.

  4. Via reinterpret_casting to char* and copying,

    float reinterpret_as_float(std::uint32_t ui) {
        char* uip = reinterpret_cast<char*>(&ui);
        float f;
        char* fp = reinterpret_cast<char*>(&f);
        for (int i = 0; i < 4; ++i) {
            fp[i] = uip[i];
        }
        return f;
    }
    

    which AFAIK is also legal, as char pointers are exempt from aliasing issues and manual byte copying loop saves a possible function call. The loop will most definitely be unrolled, yet 4 possibly separate one-byte loads/stores are worrisome, I have no idea whether this is optimizable to single four byte load/store.

The 4 is the best I've been able to come up with.

Am I correct so far? Is there a better way to do this, particulary one that will guarantee single load/store?

yuri kilochek
  • 12,709
  • 2
  • 32
  • 59
  • 1
    From where does the bit-pattern originate? – Oliver Charlesworth Dec 24 '13 at 15:05
  • 9
    `memcpy` *looks* like a function call, try that in optimized code and look at the results, it might surprise you. 4 is a violation of the strict aliasing rules (you can alias as a `char*` for reading, but not for writing) – David Rodríguez - dribeas Dec 24 '13 at 15:06
  • if the bit pattern is the set of bits of a floating-point number, why are you doing the `reinterpret_cast` to a reference, that is, interpreting the bits as a memory address and dereferencing it? – Manu343726 Dec 24 '13 at 15:08
  • @OliCharlesworth irrelevant, though you can assume it represents a valid float value. – yuri kilochek Dec 24 '13 at 15:09
  • Yep, I guess that's what I was after; it's a valid bit representation on this platform. – Oliver Charlesworth Dec 24 '13 at 15:10
  • 1
    @DavidRodríguez-dribeas: Re #4: really? – Oliver Charlesworth Dec 24 '13 at 15:13
  • 1
    @Manu343726 because that is not how reinterpret_cast works. `reinterpret_cast(e)` is equivalent to `*reinterpret_cast(&e)` as per 5.2.10/11 – yuri kilochek Dec 24 '13 at 15:14
  • Thanks, I didn't know that. I was confused with the missing & – Manu343726 Dec 24 '13 at 15:18
  • @OliCharlesworth: Yes, 3.10/10 defines the strict aliasing rules and it is stated in terms of when *access* (read) is legal. That rule enables *reading* any variable through a pointer/reference `char` (with any cv/const/mutable... combination), it does *not* provide any guarantee as of writing. – David Rodríguez - dribeas Dec 24 '13 at 15:19
  • @DavidRodríguez-dribeas is there a definition of what _access_ actually means? Previous paragraphs use _modify_ do denote writing, and I don`t see anything unambigiously defining _access_ as opposite of _modify_ and not merely a general term for reading/writing. Also would this mean that `memcpy`-like function cannot be implemented in pure c++ (i.e. loop and assign chars)? – yuri kilochek Dec 24 '13 at 15:38
  • @yurikilochek: The exact sentence is *If a program attempts to access the stored value of an object...* That looks like *read* to me, note that this is not talking about accessing the object, but the **value** that was previously **stored**. In the previous paragraph the wording used is *attempts to **modify** an object*. The wording seems to be clear in both cases. – David Rodríguez - dribeas Dec 24 '13 at 18:11
  • @yurikilochek interpreting it as "reading" will not definitely make this well-behaved. The standard says that the lifetime of an object ends when its memory is reused. It does not define what that means, but I think a reasonable way to read it is that those writes reuse the memory of the float and thereby destroy it. – Johannes Schaub - litb Dec 24 '13 at 18:23
  • But IMHO another way to read it is that if there is already an object that you write to, then this does not constitute a "reuse" of the containing object (think about arrays and their elements .. writing to an array element does definitely not destroy its containing array). In that interpretation, the code is completely valid I think, because every object of type T contains a sequence of sizeof(T) objects of type unsigned char. – Johannes Schaub - litb Dec 24 '13 at 18:26
  • I would argue that "access" describes both reading and writing. Indeed, if the rules of 3.10 aren't meant to apply to writes, then there would be no restrictions on valid writes to a modifiable glvalue. Further, it seems disingenuous to claim that aliasing as `char*` for writing is unacceptable but that `memcpy` - which does exactly that - is conforming. – Casey Dec 24 '13 at 20:50
  • I think how it got there is relevant: there are few ways the bit pattern of a `float` could be in an `int`, and at least some of them may make this question easy, and others may involve implememtation defined behaviour that implies an easy answer. – Yakk - Adam Nevraumont Dec 25 '13 at 03:11
  • @Casey what, then, constitutes an attempt to write the stored value of an object? Also, no memcpy does not alias anything with `char*`. It takes pointers to void as arguments. You seem to be assuming a particular implementation. – R. Martinho Fernandes Dec 25 '13 at 17:52
  • Have you considered a cast through `void*`? It may be no better but it's another option to add to the list. e.g. `*static_cast(static_cast(&ui));` – willj Dec 25 '13 at 22:54
  • 1
    @willj as per 5.2.10/7 this is actually exactly the same as reinterpret_cast – yuri kilochek Dec 26 '13 at 05:53
  • @DavidRodríguez-dribeas just discovered that [access is defined in intro.defs](http://eel.is/c++draft/defns.access) as both reading and writing. So option #4 appears to be valid if we do not interpret it as storage reuse. – yuri kilochek Feb 16 '17 at 07:00

4 Answers4

14

Afaik, there are only two approaches that are compliant with strict aliasing rules: memcpy() and cast to char* with copying. All others read a float from memory that belongs to an uint32_t, and the compiler is allowed to perform the read before the write to that memory location. It might even optimize away the write altogether as it can prove that the stored value will never be used according to strict aliasing rules, resulting in a garbage return value.

It really depends on the compiler/optimizes whether memcpy() or char* copy is faster. In both cases, an intelligent compiler might be able to figure out that it can just load and copy an uint32_t, but I would not trust any compiler to do so before I have seen it in the resulting assembler code.

Edit:
After some testing with gcc 4.8.1, I can say that the memcpy() approach is the best for this particulare compiler, see below for details.


Compiling

#include <stdint.h>

float foo(uint32_t a) {
    float b;
    char* aPointer = (char*)&a, *bPointer = (char*)&b;
    for( int i = sizeof(a); i--; ) bPointer[i] = aPointer[i];
    return b;
}

with gcc -S -std=gnu11 -O3 foo.c yields this assemble code:

movl    %edi, %ecx
movl    %edi, %edx
movl    %edi, %eax
shrl    $24, %ecx
shrl    $16, %edx
shrw    $8, %ax
movb    %cl, -1(%rsp)
movb    %dl, -2(%rsp)
movb    %al, -3(%rsp)
movb    %dil, -4(%rsp)
movss   -4(%rsp), %xmm0
ret

This is not optimal.

Doing the same with

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

float foo(uint32_t a) {
    float b;
    char* aPointer = (char*)&a, *bPointer = (char*)&b;
    memcpy(bPointer, aPointer, sizeof(a));
    return b;
}

yields (with all optimization levels except -O0):

movl    %edi, -4(%rsp)
movss   -4(%rsp), %xmm0
ret

This is optimal.

cmaster - reinstate monica
  • 38,891
  • 9
  • 62
  • 106
  • For reference wrt to [memcpy and strict aliasing](http://stackoverflow.com/a/31080901/1708801) a good quote from Richard Smith. – Shafik Yaghmour Sep 01 '16 at 21:02
2

If the bitpattern in the integer variable is the same as a valid float value, then union is probably the best and most compliant way to go. And it's actually legal if you read the specification (don't remember the section at the moment).

Some programmer dude
  • 400,186
  • 35
  • 402
  • 621
  • 2
    I am not sure that the `union` is the correct approach. The note in 9.5/1 seems to hint on the contrary. On the other hand, `memcpy` is correct. – David Rodríguez - dribeas Dec 24 '13 at 15:11
  • In C (which I realise is a different language), aliasing via a union is technically not legal (although universally supported). Is it explicitly legal in C++? – Oliver Charlesworth Dec 24 '13 at 15:11
  • @OliCharlesworth: I have not found an indication that it is legal, only a comment that seems to hint that it is not. *[ Note: One special guarantee is made in order to simplify the use of unions: If a standard-layout union contains several standard-layout structs that share a common initial sequence (9.2), and if an object of this standard-layout union type contains one of the standard-layout structs, it is permitted to inspect the common initial sequence of any of standard-layout struct members; see 9.2. —end note ]* Explicitly allowing that seems to mean that it is otherwise disallowed. – David Rodríguez - dribeas Dec 24 '13 at 15:15
  • 1
    @Joachim: Can you provide the section/quote that enables this in the standard? – David Rodríguez - dribeas Dec 24 '13 at 15:23
  • 1
    @OliCharlesworth С actually does allow this, see [this](http://stackoverflow.com/questions/11639947/is-type-punning-through-a-union-unspecified-in-c99-and-has-it-become-specified) – yuri kilochek Dec 24 '13 at 15:52
  • A common initial sequence would be a union containing two or more structs that each begin with an `int` member. That's not the case here - the types are explicitly not in common. – Phil Miller Apr 19 '16 at 20:47
2

memcpy is always safe but does involve a copy

casting may lead to problems

union - seems to be allowed in C99 and C11, not sure about C++

Take a look at:

What is the strict aliasing rule?

and

Is type-punning through a union unspecified in C99, and has it become specified in C11?

Community
  • 1
  • 1
doron
  • 27,972
  • 12
  • 65
  • 103
  • The copy in memcpy is irrelevant. All the options listed in the OP return copies. – R. Martinho Fernandes Dec 25 '13 at 17:54
  • @cmaster - in gcc memcpy is a built-in function, this might allow the compiler to deal with the type punning case differently. – doron Dec 31 '13 at 11:21
  • cmaster - not everyone uses gcc so the comment was still relevant. – doron Dec 31 '13 at 13:42
  • In this [answer](http://stackoverflow.com/a/20956250/1708801) I link to what I feel are the important discussion on type punning and C++. The conclusion seems to be that in practice most compilers support it but it is underspecified in the standard. – Shafik Yaghmour Jun 25 '14 at 12:15
-3
float reinterpret_as_float(std::uint32_t ui) {
   return *((float *)&ui);
}

As plain function, its code is translated into assembly as this (Pelles C for Windows):

fld [esp+4]
ret

If defined as inline function, then a code like this (n being unsigned, x being float):

x = reinterpret_as_float (n);

Is translated to assembler as this:

fld [ebp-4]  ;RHS of asignment. Read n as float
fstp dword ptr [ebp-8]  ;LHS of asignment
mcleod_ideafix
  • 11,128
  • 2
  • 24
  • 32