Home > Software engineering >  Is it always undefined behaviour to copy the bits of a variable through an incompatible pointer?
Is it always undefined behaviour to copy the bits of a variable through an incompatible pointer?

Time:04-04

For example, can this

unsigned f(float x) {
    unsigned u = *(unsigned *)&x;
    return u;
}

cause unpredictable results on a platform where,

  • unsigned and float are both 32-bit
  • a pointer has a fixed size for all types
  • unsigned and float can be stored to and loaded from the same part of memory.

I know about strict aliasing rules, but most examples showing problematic cases of violating strict aliasing is like the following.

static int g(int *i, float *f) {
    *i = 1;
    *f = 0;
    return *i;
}

int h() {
    int n;
    return g(&n, (float *)&n);
}

In my understanding, the compiler is free to assume that i and f are implicitly restrict. The return value of h could be 1 if the compiler thinks *f = 0; is redundant (because i and f can't alias), or it could be 0 if it puts into account that the values of i and f are the same. This is undefined behaviour, so technically, anything else can happen.

However, the first example is a bit different.

unsigned f(float x) {
    unsigned u = *(unsigned *)&x;
    return u;
}

Sorry for my unclear wording, but everything is done "in-place". I can't think of any other way the compiler might interpret the line unsigned u = *(unsigned *)&x;, other than "copy the bits of x to u".

In practice, all compilers for various architectures I tested in https://godbolt.org/ with full optimization produce the same result for the first example, and varying results (either 0 or 1) for the second example.

I know it's technically possible that unsigned and float have different sizes and alignment requirements, or should be stored in different memory segments. In that case even the first code won't make sense. But on most modern platforms where the following holds, is the first example still undefined behaviour (can it produce unpredictable results)?

  • unsigned and float are both 32-bit
  • a pointer has a fixed size for all types
  • unsigned and float can be stored to and loaded from the same part of memory.

In real code, I do write

unsigned f(float x) {
    unsigned u;
    memcpy(&u, &x, sizeof(x));
    return u;
}

The compiled result is the same as using pointer casting, after optimization. This question is about interpretation of the standard about strict aliasing rules for code such as the first example.

CodePudding user response:

Is it always undefined behaviour to copy the bits of a variable through an incompatible pointer?

Yes.

The rule is https://port70.net/~nsz/c/c11/n1570.html#6.5p7 :

An object shall have its stored value accessed only by an lvalue expression that has one of the following types:

  • a type compatible with the effective type of the object,
  • a qualified version of a type compatible with the effective type of the object,
  • a type that is the signed or unsigned type corresponding to the effective type of the object,
  • a type that is the signed or unsigned type corresponding to a qualified version of the effective type of the object,
  • an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union), or
  • a character type.

The effective type of the object x is float - it is defined with that type.

  • unsigned is not compatible with float,
  • unsigned is not a qualified version of float,
  • unsigned is not a signed or unsigned type of float,
  • unsigned is not a signed or unsigned type corresponding to qualified version of float,
  • unsigned is not an aggregate or union type
  • and unsigned is not a character type.

The "shall" is violated, it is undefined behavior (see https://port70.net/~nsz/c/c11/n1570.html#4p2 ). There is no other interpretation.

We also have https://port70.net/~nsz/c/c11/n1570.html#J.2 :

The behavior is undefined in the following circumstances:

  • An object has its stored value accessed other than by an lvalue of an allowable type (6.5).

CodePudding user response:

As Kamil explains, it's UB. Even int and long (or long and long long) aren't alias-compatible even when they're the same size. (But interestingly, unsigned int is compatible with int)

It's nothing to do with being the same size, or using the same register-set as suggested in a comment, it's mainly a way to let compilers assume that different pointers don't point to overlapping memory when optimizing. They still have to support C99 union type-punning, not just memcpy. So for example a dst[i] = src[i] loop doesn't need to check for possible overlap when unrolling or vectorizing, if dst and src have different types.1

If you're accessing the same integer data, the standard requires that you use the exact same type, modulo only things like signed vs. unsigned and const. Or that you use (unsigned) char*, which is like GNU C __attribute__((may_alias)).


The other part of your question seems to be why it appears to work in practice, despite the UB.
Your godbolt link forgot to link the actual compilers you tried.

https://godbolt.org/z/rvj3d4e4o shows GCC4.1, from before GCC went out of its way to support "obvious" local compile-time-visible cases like this, to sometimes not break people's buggy code using non-portable idioms like this. It loads garbage from stack memory, unless you use -fno-strict-aliasing to make it movd to that location first. (Store/reload instead of movd %xmm0,

  • Related