gcc, strict-aliasing, and casting through a union
The fact that GCC is warning about unions doesn't necessarily mean that unions don't currently work. But here's a slightly less simple example than yours:
#include <stdio.h>
struct B {
int i1;
int i2;
};
union A {
struct B b;
double d;
};
int main() {
double d = 3.0;
#ifdef USE_UNION
((union A*)&d)->b.i2 += 0x80000000;
#else
((int*)&d)[1] += 0x80000000;
#endif
printf("%g\n", d);
}
Output:
$ gcc --version
gcc (GCC) 4.3.4 20090804 (release) 1
Copyright (C) 2008 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ gcc -oalias alias.c -O1 -std=c99 && ./alias
-3
$ gcc -oalias alias.c -O3 -std=c99 && ./alias
3
$ gcc -oalias alias.c -O1 -std=c99 -DUSE_UNION && ./alias
-3
$ gcc -oalias alias.c -O3 -std=c99 -DUSE_UNION && ./alias
-3
So on GCC 4.3.4, the union "saves the day" (assuming I want the output "-3"). It disables the optimisation that relies on strict aliasing and that results in the output "3" in the second case (only). With -Wall, USE_UNION also disables the type-pun warning.
I don't have gcc 4.4 to test, but please give this code a go. Your code in effect tests whether the memory for d
is initialised before reading back through a union: mine tests whether it is modified.
Btw, the safe way to read half of a double as an int is:
double d = 3;
int i;
memcpy(&i, &d, sizeof i);
return i;
With optimisation on GCC, this results in:
int thing() {
401130: 55 push %ebp
401131: 89 e5 mov %esp,%ebp
401133: 83 ec 10 sub $0x10,%esp
double d = 3;
401136: d9 05 a8 20 40 00 flds 0x4020a8
40113c: dd 5d f0 fstpl -0x10(%ebp)
int i;
memcpy(&i, &d, sizeof i);
40113f: 8b 45 f0 mov -0x10(%ebp),%eax
return i;
}
401142: c9 leave
401143: c3 ret
So there's no actual call to memcpy. If you aren't doing this, you deserve what you get if union casts stop working in GCC ;-)
Well it's a bit of necro-posting, but here is a horror story. I'm porting a program that was written with the assumption that the native byte order is big endian. Now I need it to work on little endian too. Unfortunately, I can't just use native byte order everywhere, as data could be accessed in many ways. For example, a 64-bit integer could be treated as two 32-bit integers or as 4 16-bit integers, or even as 16 4-bit integers. To make things worse, there is no way to figure out what exactly is stored in memory, because the software is an interpreter for some sort of byte code, and the data is formed by that byte code. For example, the byte code may contain instructions to write an array of 16-bit integers, and then access a pair of them as a 32-bit float. And there is no way to predict it or alter the byte code.
Therefore, I had to create a set of wrapper classes to work with values stored in the big endian order regardless of the native endianness. Worked perfectly in Visual Studio and in GCC on Linux with no optimizations. But with gcc -O2, hell broke loose. After a lot of debugging I figured out that the reason was here:
double D;
float F;
Ul *pF=(Ul*)&F; // Ul is unsigned long
*pF=pop0->lu.r(); // r() returns Ul
D=(double)F;
This code was used to convert a 32-bit representation of a float stored in a 32-bit integer to double. It seems that the compiler decided to do the assignment to *pF after the assignment to D - the result was that the first time the code was executed, the value of D was garbage, and the consequent values were "late" by 1 iteration.
Miraculously, there were no other problems at that point. So I decided to move on and test my new code on the original platform, HP-UX on a RISC processor with native big endian order. Now it broke again, this time in my new class:
typedef unsigned long long Ur; // 64-bit uint
typedef unsigned char Uc;
class BEDoubleRef {
double *p;
public:
inline BEDoubleRef(double *p): p(p) {}
inline operator double() {
Uc *pu = reinterpret_cast<Uc*>(p);
Ur n = (pu[7] & 0xFFULL) | ((pu[6] & 0xFFULL) << 8)
| ((pu[5] & 0xFFULL) << 16) | ((pu[4] & 0xFFULL) << 24)
| ((pu[3] & 0xFFULL) << 32) | ((pu[2] & 0xFFULL) << 40)
| ((pu[1] & 0xFFULL) << 48) | ((pu[0] & 0xFFULL) << 56);
return *reinterpret_cast<double*>(&n);
}
inline BEDoubleRef &operator=(const double &d) {
Uc *pc = reinterpret_cast<Uc*>(p);
const Ur *pu = reinterpret_cast<const Ur*>(&d);
pc[0] = (*pu >> 56) & 0xFFu;
pc[1] = (*pu >> 48) & 0xFFu;
pc[2] = (*pu >> 40) & 0xFFu;
pc[3] = (*pu >> 32) & 0xFFu;
pc[4] = (*pu >> 24) & 0xFFu;
pc[5] = (*pu >> 16) & 0xFFu;
pc[6] = (*pu >> 8) & 0xFFu;
pc[7] = *pu & 0xFFu;
return *this;
}
inline BEDoubleRef &operator=(const BEDoubleRef &d) {
*p = *d.p;
return *this;
}
};
For some really weird reason, the first assignment operator only correctly assigned bytes 1 through 7. Byte 0 always had some nonsense in it, which broke everything as there is a sign bit and a part of order.
I have tried to use unions as a workaround:
union {
double d;
Uc c[8];
} un;
Uc *pc = un.c;
const Ur *pu = reinterpret_cast<const Ur*>(&d);
pc[0] = (*pu >> 56) & 0xFFu;
pc[1] = (*pu >> 48) & 0xFFu;
pc[2] = (*pu >> 40) & 0xFFu;
pc[3] = (*pu >> 32) & 0xFFu;
pc[4] = (*pu >> 24) & 0xFFu;
pc[5] = (*pu >> 16) & 0xFFu;
pc[6] = (*pu >> 8) & 0xFFu;
pc[7] = *pu & 0xFFu;
*p = un.d;
but it didn't work either. In fact, it was a bit better - it only failed for negative numbers.
At this point I'm thinking about adding a simple test for native endianness, then doing everything via char*
pointers with if (LITTLE_ENDIAN)
checks around. To make things worse, the program makes heavy use of unions all around, which seems to work ok for now, but after all this mess I won't be surprised if it suddenly breaks for no apparent reason.
Your assertion that the following code is "wrong":
extern void foo(int *, double *);
union a_union t;
t.d = 3.0;
foo(&t.i, &t.d); // undefined behavior
... is wrong. Just taking the address of the two union members and passing them to an external function doesn't result in undefined behaviour; you only get that from dereferencing one of those pointers in an invalid way. For instance if the function foo returns immediately without dereferencing the pointers you passed it, then the behaviour is not undefined. With a strict reading of the C99 standard, there are even some cases where the pointers can be dereferenced without invoking undefined behaviour; for instance, it could read the value referenced by the second pointer, and then store a value through the first pointer, as long as they both point to a dynamically allocated object (i.e. one without a "declared type").