Given the code:
struct s1 {unsigned short x;};
struct s2 {unsigned short x;};
union s1s2 { struct s1 v1; struct s2 v2; };
static int read_s1x(struct s1 *p) { return p->x; }
static void write_s2x(struct s2 *p, int v) { p->x=v;}
int test(union s1s2 *p1, union s1s2 *p2, union s1s2 *p3)
{
if (read_s1x(&p1->v1))
{
unsigned short temp;
temp = p3->v1.x;
p3->v2.x = temp;
write_s2x(&p2->v2,1234);
temp = p3->v2.x;
p3->v1.x = temp;
}
return read_s1x(&p1->v1);
}
int test2(int x)
{
union s1s2 q[2];
q->v1.x = 4321;
return test(q,q+x,q+x);
}
#include <stdio.h>
int main(void)
{
printf("%d\n",test2(0));
}
There exists one union object in the entire program--q
. Its active member is set to v1
, and then to v2
, and then to v1
again. Code only uses the address-of operator on q.v1
, or the resulting pointer, when that member is active, and likewise q.v2
. Since p1
, p2
, and p3
are all the same type, it should be perfectly legal to use p3->v1
to access p1->v1
, and p3->v2
to access p2->v2
.
I don't see anything that would justify a compiler failing to output 1234, but many compilers including clang and gcc generate code that outputs 4321. I think what's going on is that they decide that the operations on p3 won't actually change the contents of any bits in memory, they can just be ignored altogether, but I don't see anything in the Standard that would justify ignoring the fact that p3
is used to copy data from p1->v1
to p2->v2
and vice versa.
Is there anything in the Standard that would justify such behavior, or are compilers simply not following it?
I believe that your code is conformant, and there is a flaw with the -fstrict-aliasing
mode of GCC and Clang.
I cannot find the right part of the C standard, but the same problem happens when compiling your code in C++ mode for me, and I did find the relevant passages of the C++ Standard.
In the C++ standard, [class.union]/5 defines what happens when operator =
is used on a union access expression. The C++ Standard states that when a union is involved in the member access expression of the built-in operator =
, the active member of the union is changed to the member involved in the expression (if the type has a trivial constructor, but because this is C code, it does have a trivial constructor).
Note that write_s2x
cannot change the active member of the union, because a union is not involved in the assignment expression. Your code does not assume that this happens, so it's OK.
Even if I use placement new
to explicitly change which union member is active, which ought to be a hint to the compiler that the active member changed, GCC still generates code that outputs 4321
.
This looks like a bug with GCC and Clang assuming that the switching of active union member cannot happen here, because they fail to recognize the possibility of p1
, p2
and p3
all pointing to the same object.
GCC and Clang (and pretty much every other compiler) support an extension to C/C++ where you can read an inactive member of a union (getting whatever potentially garbage value as a result), but only if you do this access in a member access expression involving the union. If v1
were not the active member, read_s1x
would not be defined behavior under this implementation-specific rule, because the union is not within the member access expression. But because v1
is the active member, that shouldn't matter.
This is a complicated case, and I hope that my analysis is correct, as someone who isn't a compiler maintainer or a member of one of the committees.