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 didn't read the standard, but playing with pointers in a strict-aliasing mode (ie, using
-fstrict-alising
) is dangerous. See the gcc online doc:Found anything similar in the second example huh?
With a strict interpretation of the standard, this code might be not conforming. Let's focus on the text of the well-known §6.5p7:
(emphasis mine)
Your functions
read_s1x()
andwrite_s2x()
do the opposite of what I marked bold above in the context of your whole code. With just this paragraph, you could conclude that it's not allowed: A pointer tounion s1s2
would be allowed to alias a pointer tostruct s1
, but not vice versa.This interpretation of course would mean that the code must work as intended if you "inline" these functions manually in your
test()
. This is indeed the case here with gcc 6.2 fori686-w64-mingw32
.Adding two arguments in favor of the strict interpretation presented above:
While it's always allowed to alias any pointer with
char *
, a character array can't be aliased by any other type.Considering the (here unrelated) §6.5.2.3p6:
(again emphasis mine) -- the typical interpretation is that being visible means directly in the scope of the function in question, not "somewhere in the translation unit" ... so this guarantee doesn't include a function that takes a pointer to one of the
struct
s that's a member of theunion
.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 outputs4321
.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
andp3
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 becausev1
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.
It is not about conforming or not conforming - it one of the optimisation "traps". All of your data structures have been optimised out and you pass the same pointer to optimised out data so the the execution tree is reduced to simple printf of the value.
to change it you need to make this "transfer" function to be side effect prone and force the real assignments. It will force optimizer to not reduce those nodes in the execution tree:
it is quite trivial test just artificially made a bit more complicated.