Is there something wrong with the code?
For practical purposes, yes.
I think this is the same underlying issue as Is it undefined behaviour to call a function with pointers to different elements of a union as arguments?
As Eric Postpischil points out, the C standard as read literally seems to permit your code, and require it to print out 6 (assuming that's consistent with how your implementation represents integer types and how it lays out unions). However, this literal reading would render the strict aliasing rule almost entirely impotent, so in my opinion it's not what the standard authors would have intended.
The spirit of the strict aliasing rule is that the same object may not be accessed through pointers to different types (with certain exceptions for character types, etc) and that the compiler may optimize on the assumption that this never happens. Although d.a
and d.c
are not strictly speaking "the same object", they do have overlapping storage, and I think compiler authors interpret the rule as also not allowing overlapping objects to be accessed through pointers to different types. Under that interpretation your code would have undefined behavior.
In Defect Report 236 the committee considered a similar example and stated that it has undefined behavior, because of its use of pointers that "have different types but designate the same region of storage". However, wording to clarify this does not seem to have ever made it into any subsequent version of the standard.
Anyhow, I think the practical upshot is that you cannot expect your code to work "correctly" under modern compilers that enforce their interpretations of the strict aliasing rule. Whether or not this is a clang bug is a matter of opinion, but even if you do think it is, then it's a bug that they are probably not ever going to fix.
Why does it behave this way?
If you use the -fno-strict-aliasing
flag, then you get back to the 6 behavior. My guess is that the sanitizers happen to inhibit some of these optimizations, which is why you don't see the 0 behavior when using those options.
What seems to have happened under the hood with -O1
is the compiler assumed that the stores to *h
and *e
don't interact (because of their different types) and therefore can be freely reordered. So it hoisted *h = g
outside the loop, since after all multiple stores to the same address, with no intervening load, are redundant and only the last one needs to be kept. It happened to put it after the loop, presumably because it can't prove that e
doesn't point to g
, so the value of g
needs to be reloaded after the loop. So the final value of d.b
is derived from *h = g
which effectively does d.a = 0
.
How to get a warning?
Unfortunately, compilers are not good at checking, either statically or at runtime, for violations of (their interpretation of) the strict aliasing rule. I'm not aware of any way to get a warning for such code. With clang you can use -Weverything
to enable every warning option that it supports (many of which are useless or counterproductive), and even with that, it gives no relevant warnings about your program.
Another example
In case anyone is curious, here's another test case that doesn't rely on any type pun, reinterpretation, or other implementation-defined behavior.
#include <stdio.h>
short int zero = 0;
void a(int *pi, long *pl) {
for (int x = 0; x < 1000; x++) {
*pl = x;
*pi = zero;
}
}
int main(void) {
union { int i; long l; } u;
a(&u.i, &u.l);
printf("%d\n", u.i);
}
Try on godbolt
As read literally, this code would appear to print 0 on any implementation: the last assignment in a()
was to u.i
, so u.i
should be the active member, and the printf
should output the value 0 which was assigned to it. However, with clang -O2
, the stores are reordered and the program outputs 999
.
Just as a counterpoint, though, if you read the standard so as to make the above example UB, then this leads to the somewhat absurd conclusion that u.l = 0; u.i = 5; print(u.i);
is well defined and prints 5, but that *&u.l = 0; *&u.i = 5; print(u.i);
is UB. (Recall that the "cancellation rule" of &
and *
applies to &*p
but not to *&x
.)
The whole situation is rather unsatisfactory.