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

  • A+
Category:Languages

I have a problem understanding what can and cannot be done using unions with GCC. I read the questions (in particular here and here) about it but they focus the C++ standard, I feel there's a mismatch between the C++ standard and the practice (the commonly used compilers).

In particular, I recently found confusing informations in the GCC online doc while reading about the compilation flag -fstrict-aliasing. It says:


-fstrict-aliasing

Allow the compiler to assume the strictest aliasing rules applicable to the language being compiled. For C (and C++), this activates optimizations based on the type of expressions. In particular, an object of one type is assumed never to reside at the same address as an object of a different type, unless the types are almost the same. For example, an unsigned int can alias an int, but not a void* or a double. A character type may alias any other type. Pay special attention to code like this:

union a_union {   int i;   double d; };  int f() {   union a_union t;   t.d = 3.0;   return t.i; } 

The practice of reading from a different union member than the one most recently written to (called “type-punning”) is common. Even with -fstrict-aliasing, type-punning is allowed, provided the memory is accessed through the union type. So, the code above works as expected.


This is what I think I understood from this example and my doubts:

1) aliasing only works between similar types, or char

Consequence of 1): aliasing - as the word suggests - is when you have one value and two members to access it (i.e. the same bytes);

Doubt: are two types similar when they have the same size in bytes? If not, what are similar types?

Consequence of 1) for non similar types (whatever this means), aliasing does not work;

2) type punning is when we read a different member than the one we wrote to; it's common and it works as expected as long as the memory is accessed through the union type;

Doubt: is aliasing a specific case of type-punning where types are similar?

I get confused because it says unsigned int and double are not similar, so aliasing does not work; then in the example it's aliasing between int and double and it clearly says it works as expected, but calls it type-punning: not because types are or are not similar, but because it's reading from a member it did not write. But reading from a member it did not write is what I understood aliasing is for (as the word suggests). I'm lost.

The questions: can someone clarify the difference between aliasing and type-punning and what uses of the two techniques are working as expected in GCC? And what does the compiler flag do?

 


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 {     float f;     struct {         unsigned sig : 1, exp : 8, man : 23;     } parts; } u = {2.5f}; u.parts.sig = 1; 

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

Comment

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: