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:
- unspecified behavior
- strict aliasing
- lifetime issues
- endianness
- alignment
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:
reinterpret_cast
and c-style cast[[nodiscard]] float int_to_float1(int x) noexcept { return *reinterpret_cast<float*>(&x); } [[nodiscard]] float int_to_float2(int x) noexcept { return *(float*)(&x); }
static_cast
andvoid*
[[nodiscard]] float int_to_float3(int x) noexcept { return *static_cast<float*>(static_cast<void*>(&x)); }
std::bit_cast
[[nodiscard]] constexpr float int_to_float4(int x) noexcept { return std::bit_cast<float>(x); }
memcpy
[[nodiscard]] float int_to_float5(int x) noexcept { float destination; memcpy(&destination, &x, sizeof(x)); return destination; }
union
[[nodiscard]] float int_to_float6(int x) noexcept { union { int as_int; float as_float; } destination{x}; return destination.as_float; }
placement
new
andstd::launder
[[nodiscard]] float int_to_float7(int x) noexcept { new(&x) float; return *std::launder(reinterpret_cast<float*>(&x)); }
std::byte
[[nodiscard]] float int_to_float8(int x) noexcept { return *reinterpret_cast<float*>(reinterpret_cast<std::byte*>(&x)); }
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)
std::bit_cast
andmemcpy
are not UB. – Moorlandmemcpy
variant. It is well-defined and the compiler can and will optimize it to do the right thing. – Penatesstd::bit_cast
is only C++20 and later... But is certainly the modern way. – Pyrogallol