janekb04
janekb04

Reputation: 4915

What is the modern, correct way to do type punning in C++?

It seems like there are two types of C++. The practical C++ and the language lawyer C++. In certain situations, it can be useful to be able to interpret a bit pattern of one type as if it were a different type. Floating-point tricks are a notable example. Let's take the famous fast inverse square root (taken from Wikipedia, which was in turn taken from here):

float Q_rsqrt( float number )
{
    long i;
    float x2, y;
    const float threehalfs = 1.5F;

    x2 = number * 0.5F;
    y  = number;
    i  = * ( long * ) &y;                       // evil floating point bit level hacking
    i  = 0x5f3759df - ( i >> 1 );               // what the
    y  = * ( float * ) &i;
    y  = y * ( threehalfs - ( x2 * y * y ) );   // 1st iteration
//  y  = y * ( threehalfs - ( x2 * y * y ) );   // 2nd iteration, this can be removed

    return y;
}

Setting aside details, it uses certain properties of the IEEE-754 floating-point bit representation. The interesting part here is the *(long*) cast from float* to long*. There are differences between C and C++ about which types of such reinterpreting casts are defined behavior, however in practice such techniques are used often in both languages.

The thing is that for such a simple problem there are a lot of pitfalls that can occur with the approach presented above and different others. To name some:

At the same time, there are a lot of ways of performing type punning and a lot of mechanisms related to it. These are all that I could find:

The question is which of these ways are safe, which are unsafe, and which are damned forever. Which one should be used and why? Is there a canonical one accepted by the C++ community? Why are new versions of C++ introducing even more mechanisms std::launder in C++17 or std::byte, std::bit_cast in C++20?

To give a concrete problem: what would be the safest, most performant, and best way to rewrite the fast inverse square root function? (Yes, I know that there is a suggestion of one way on Wikipedia).

Edit: To add to the confusion, it seems that there is a proposal that suggests adding yet another type punning mechanism: std::start_lifetime_as, which is also discussed in another question.

(godbolt)

Upvotes: 56

Views: 7203

Answers (2)

463035818_is_not_an_ai
463035818_is_not_an_ai

Reputation: 122458

This is what I get from gcc 11.1 with -O3:

int_to_float4(int):
        movd    xmm0, edi
        ret
int_to_float1(int):
        movd    xmm0, edi
        ret
int_to_float2(int):
        movd    xmm0, edi
        ret
int_to_float3(int):
        movd    xmm0, edi
        ret
int_to_float5(int):
        movd    xmm0, edi
        ret
int_to_float6(int):
        movd    xmm0, edi
        ret
int_to_float7(int):
        mov     DWORD PTR [rsp-4], edi
        movss   xmm0, DWORD PTR [rsp-4]
        ret
int_to_float8(int):
        movd    xmm0, edi
        ret

I had to add a auto x = &int_to_float4; to force gcc to actually emit anything for int_to_float4, I guess thats the reason it appears first.

Live Example

I am not that familiar with std::launder so I cannot tell why it is different. Otherwise they are identical. This is what gcc has to say about it (in this context, with that flags). What the standard says is different story. Though, memcpy(&destination, &x, sizeof(x)); is well defined and most compilers know how to optimize it. std::bit_cast was introduced in C++20 to make such casts more explicit. Note that in the possible implementation on cppreference they use std::memcpy ;).


TL;DR

what would be the safest, most performant and best way to rewrite the fast inverse square root function?

std::memcpy and in C++20 and beyond std::bit_cast.

Upvotes: 14

Jérôme Richard
Jérôme Richard

Reputation: 50358

First of all, you assume that sizeof(long) == sizeof(int) == sizeof(float). This is not always true, and totally unspecified (platform dependent). Actually, this is true on my Windows using clang-cl and wrong on my Linux using the same 64-bit machine. Different compilers on the same OS/machine can give different results. A static assert is at least required to avoid sneaky bugs.

Plain C casts, reinterpret casts and static casts are invalid here because of the strict aliasing rule (to be pedantic, the program is ill-formed in this case regarding the C++ standard). The union solution is not valid too (it is only valid in C, not in C++). Only the std::bit_cast and the std::memcpy solution are "safe" (assuming the size of the types are matching on the target plateform). Using std::memcpy is often fast as it is optimized by most mainstream compiler (when optimizations are enabled, like with -O3 for GCC/Clang): the std::memcpy call can be inlined and replaced by faster instructions. std::bit_cast is the new way of doing this (only since C++20). The last solution is cleaner for a C++ code as std::memcpy use unsafe void* types and thus by-pass the type system.

Upvotes: 17

Related Questions