This is mostly a duplicate question, but I am going to write out an answer to it anyway, because I can't find an earlier answer that discusses the specific issues of casting through void *
and separate compilation.
First off, let us imagine a simpler version of your code:
#include <stddef.h>
#include <stdint.h>
extern void func_takes_word(uint32_t word);
struct __attribute__((packed, aligned(_Alignof(uint32_t)))) some_struct
{
uint32_t num_0;
uint32_t num_1;
uint32_t num_2;
};
void some_func(void)
{
struct some_struct stc = {0, 1, 59};
for (size_t i = 0; i < sizeof(struct some_struct) / sizeof(uint32_t); i++)
func_takes_word(*(((uint32_t *)&stc) + i));
}
(The GCC __attribute__((packed, aligned(...)))
annotation is present only to exclude the possibility of any problems due to padding or misalignment. Everything I say below would still be true if you took it out.)
According to the most straightforward interpretation of C2011 as written, this code does violate the "strict aliasing" rules (N1570: 6.2.7 and 6.5p6,7). The type struct some_struct
is not compatible with the type uint32_t
. Therefore, taking the address of an object with declared type struct some_struct
, casting the resultant pointer to type uint32_t *
, adding a nonzero offset, and dereferencing the cast pointer, has undefined behavior. It really is that simple. (EDIT: If the pointer is not offset, the dereference has well-defined behavior, because of a special case rule hiding in section 6.7.2p15 which I totally forgot about. Thanks to dbush for pointing this out.)
Many people angrily resist this interpretation of the standard and insist that the committee must have meant something else, because there are millions, if not billions, of lines of "legacy" C code out there that do exactly the above and expect it to work. Not to mention that it's unclear how you could do anything useful whatsoever with offsetof
under this interpretation. But the text really does say this, there's no other plausible interpretation, and the wording of the relevant sections of the standard has been mostly unchanged since the original 1989 ANSI C. I think we have to assume that the committee's lack of interest in changing the text, for thirty years now, despite several formal requests for clarification or correction, means it says what they wanted it to say.
Now, regarding casting through void *
and/or splitting up the operations so that the original "effective type" of the object is not visible to the code that performs the dereference: These make no difference. Your original pair of translation units still has undefined behavior.
Casts through void *
make no difference because the rules in section 6.5.p6 say nothing about intermediate casts. They only talk about the "effective type" of the actual object in memory, and the type of the lvalue expression used to access the object. So, it doesn't matter what types the pointer may have had in between the time when the object's address was taken, and the time when the pointer was dereferenced (as long as none of the casts destroy information, which is guaranteed not to happen for casts from object types to void *
and back).
Splitting the operations up, so that the original "effective type" of the object is not visible (statically) to the code that performs the dereference, makes no difference because the C standard places no limits whatsoever on the sophistication of the analysis the compiler is allowed to perform before deciding whether an access is allowed. In particular, an implementation that tags every byte of memory with its "effective type", and performs runtime checks on every dereference, has been explicitly endorsed by the committee (not in the text of the standard, but in DR responses, I don't remember how long ago this was and WG14's website is not very searchable). An implementation is also allowed to do arbitrarily aggressive inlining and interprocedural analysis, during translation phase 8 ("link-time optimization") as well as phase 7. Collapsing your original program into my "simpler version" is well within the capabilities of current-generation whole-program optimizing compilers.
As pointed out in the comments on the question, you may be able to rely on knowledge of how sophisticated a specific implementation's optimizer is, or on an implementation's overt extensions (e.g. __attribute__((noinline))
) to control whether or not you get machine code that behaves as intended despite the undefined behavior. The C standard even explicitly licenses you to do this, by defining a distinction between a "conforming program" and a "strictly conforming program" (N1570: section 4). A program that relies on one particular implementation's treatment of undefined behavior can still be conforming but is not strictly conforming, and its authors have to be aware that it might break when ported to a different implementation (including, perhaps, a newer version of the same compiler).
*(uint32_t *)obj
or((uint32_t *)obj)[0]
is valid but((uint32_t *)obj)[1]
is not. – Deteriorate