Opinions on Type-Punning in C++

Opinions on type-punning in C++?

As far as the C++ standard is concerned, litb's answer is completely correct and the most portable. Casting const char *data to a const uint3_t *, whether it be via a C-style cast, static_cast, or reinterpret_cast, breaks the strict aliasing rules (see Understanding Strict Aliasing). If you compile with full optimization, there's a good chance that the code will not do the right thing.

Casting through a union (such as litb's my_reint) is probably the best solution, although it does technically violate the rule that if you write to a union through one member and read it through another, it results in undefined behavior. However, practically all compilers support this, and it results in the the expected result. If you absolutely desire to conform to the standard 100%, go with the bit-shifting method. Otherwise, I'd recommend going with casting through a union, which is likely to give you better performance.

Why is type punning considered UB?

Ultimately the why is "because the language specification says so". You don't get to argue with that. If that's the way the language is, it's the way it is.

If you want to know the motivation for making it that way, it's that the original C language lacked any way of expressing that two lvalues can't alias one another (and the modern language's restrict keyword is still barely understood by most users of the language). Being unable to assume two lvalues can't alias means the compiler can't reorder loads and stores, and must actually perform loads and stores from/to memory for every access to an object, rather than keeping values in registers, unless it knows the object's address has never been taken.

C's type-based aliasing rules somewhat mitigate this situation, by letting the compiler assume lvalues with different types don't alias.

Note also that in your example, there's not only type-punning but misalignment. The unsigned char array has no inherent alignment, so accessing a uint64_t at that address would be an alignment error (UB for another reason) independent of any aliasing rules.

Unions and type-punning

To re-iterate, type-punning through unions is perfectly fine in C (but not in C++). In contrast, using pointer casts to do so violates C99 strict aliasing and is problematic because different types may have different alignment requirements and you could raise a SIGBUS if you do it wrong. With unions, this is never a problem.

The relevant quotes from the C standards are:

C89 section 3.3.2.3 §5:

if a member of a union object is accessed after a value has been stored in a different member of the object, the behavior is implementation-defined

C11 section 6.5.2.3 §3:

A postfix expression followed by the . operator and an identifier designates a member of a structure or union object. The value is that of the named member

with the following footnote 95:

If the member used to read the contents of a union object is not the same as the member last used to store a value in the object, the appropriate part of the object representation of the value is reinterpreted as an object representation in the new type as described in 6.2.6 (a process sometimes called ‘‘type punning’’). This might be a trap representation.

This should be perfectly clear.


James is confused because C11 section 6.7.2.1 §16 reads

The value of at most one of the members can be stored in a union object at any time.

This seems contradictory, but it is not: In contrast to C++, in C, there is no concept of active member and it's perfectly fine to access the single stored value through an expression of an incompatible type.

See also C11 annex J.1 §1:

The values of bytes that correspond to union members other than the one last stored into [are unspecified].

In C99, this used to read

The value of a union member other than the last one stored into [is unspecified]

This was incorrect. As the annex isn't normative, it did not rate its own TC and had to wait until the next standard revision to get fixed.


GNU extensions to standard C++ (and to C90) do explicitly allow type-punning with unions. Other compilers that don't support GNU extensions may also support union type-punning, but it's not part of the base language standard.

How to tell if type punning is ok on my platform?

Q1: Is there a way to test if type punning is supported and behaves as expected on my platform?

Yes, read the compiler manual. There is usually a chapter about non-standard extensions. For example gcc C extensions or gcc C++ extensions. If there is no documented non-standard extension (or in case you compile for strict standard compliance), you have to assume that using such code is unsafe and undefined behavior.

gcc in particular is traditionally infamous for invoking undefined behavior upon strict pointer aliasing violations. You can block it from exploiting that UB during optimizations with -f-no-strict-aliasing.

Type punning through unions is well-defined in C but undefined in C++.

Q2: Can I expect mainstream compilers (MSVC, GCC) to warn or error on illegal type punning on my platform?

No, this is very rare. In general, you can't expect diagnostic messages upon invoking undefined behavior. This is why there's still a market for static analyser tools. Also, many forms of undefined behavior occur in run-time.

Unions, aliasing and type-punning in practice: what works and what does not?

Aliasing can be taken literally for what it means: it is when two different expressions refer to the same object. Type-punning is to "pun" a type, ie to use a object of some type as a different type.

Formally, type-punning is undefined behaviour with only a few exceptions. It happens commonly when you fiddle with bits carelessly

int mantissa(float f)
{
return (int&)f & 0x7FFFFF; // Accessing a float as if it's an int
}

The exceptions are (simplified)

  • Accessing integers as their unsigned/signed counterparts
  • Accessing anything as a char, unsigned char or std::byte

This is known as the strict-aliasing rule: the compiler can safely assume two expressions of different types never refer to the same object (except for the exceptions above) because they would otherwise have undefined behaviour. This facilitates optimizations such as

void transform(float* dst, const int* src, int n)
{
for(int i = 0; i < n; i++)
dst[i] = src[i]; // Can be unrolled and use vector instructions
// If dst and src alias the results would be wrong
}

What gcc says is it relaxes the rules a bit, and allows type-punning through unions even though the standard doesn't require it to

union {
int64_t num;
struct {
int32_t hi, lo;
} parts;
} u = {42};
u.parts.hi = 420;

This is the type-pun gcc guarantees will work. Other cases may appear to work but may one day silently be broken.

Type punning and Unions in C

Yes, storing one member of union and reading another is type punning (assuming the types are sufficiently different). Moreover, this is the only kind of universal (any type to any type) type punning that is officially supported by C language. It is supported in a sense that the language promises that in this case the type punning will actually occur, i.e. that a physical attempt to read an object of one type as an object of another type will take place. Among other things it means that writing one member of the union and reading another member implies a data dependency between the write and the read. This, however, still leaves you with the burden of ensuring that the type punning does not produce a trap representation.

When you use casted pointers for type punning (what is usually understood as "classic" type punning), the language explicitly states that in general case the behavior is undefined (aside from reinterpreting object's value as an array of chars and other restricted cases). Compilers like GCC implement so called "strict aliasing semantics", which basically means that the pointer-based type punning might not work as you expect it to work. For example, the compiler might (and will) ignore the data dependency between type-punned reads and writes and rearrange them arbitrarily, thus completely ruining your intent. This

int i;
float f;

i = 5;
f = *(float *) &i;

can be easily rearranged into actual

f = *(float *) &i;
i = 5;

specifically because a strict-aliased compiler deliberately ignores the possibility of data dependency between the write and the read in the example.

In a modern C compiler, when you really need to perform physical reinterpretation of one objects value as value of another type, you are restricted to either memcpy-ing bytes from one object to another or to union-based type punning. There are no other ways. Casting pointers is no longer a viable option.

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

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.



Related Topics



Leave a reply



Submit