The Standard, at least as interpreted by clang and gcc in -fstrict-aliasing
mode, does not allow for storage which has been used as one type to be reliably reused as a different type within its lifetime, regardless of where the storage came from, and even if such storage was originally obtained via malloc. The inability to reliably form untyped memory pools of static duration flows generally from the inability to have reliable memory pools of any duration.
According to paragraph 6 of N1570 6.5:
The effective type of an object for an access to its stored value is the declared type of the
object, if any.87) If a value is stored into an object having no declared type through an
lvalue having a type that is not a character type, then the type of the lvalue becomes the
effective type of the object for that access and for subsequent accesses that do not modify
the stored value. If a value is copied into an object having no declared type using
memcpy or memmove, or is copied as an array of character type, then the effective type
of the modified object for that access and for subsequent accesses that do not modify the
value is the effective type of the object from which the value is copied, if it has one. For
all other accesses to an object having no declared type, the effective type of the object is
simply the type of the lvalue used for the access.
Footnote 87 reads:
Footnote 87) Allocated objects have no declared type.
but it doesn't specify where or not there might be other cases where objects would have or not be regarded as having a declared type. Given something like:
extern int x;
extern int *alloc = malloc(sizeof (int));
extern int *unknownExternalFunction(int*p1, int *p2);
extern short s;
int *p = unknownExternalFunction(&x, alloc);
memcpy(p, &s, sizeof (short));
it would seem like *p
should have a declared type in cases where unknownExternalFunction
happens to return the first argument, but not in cases where it returns the second. Thus, the effective type of the storage at p
should be int
in the first scenario, but short
in the second. From what I can tell, with the rules as written, a compiler would clearly be entitled to generate code that checks whether the returned address is coincidentally equal to x
or p
, and selects among versions of the downstream code optimized for scenarios where the memcpy
does or doesn't alter the value. It turns out, though, that there's an even more interesting subtlety.
Although the notion of "modify" used elsewhere in the Standard indicates that even an action which writes an object with the value it already holds "modifies" it, the meaning here is different, and an object need not be regarded as modified by a sequence of actions that leave it holding the same bit pattern as it held before.
struct s { int x; };
struct t { int x; };
int test(void*p, void *q)
{
struct s *ps = p;
ps->x = 1;
struct t *qt = q;
qt->x = 2;
struct t *pt = p;
int temp = pt->x;
// Start of sequence that writes to *p
// but leaves bit pattern unchanged
ps->x = 49;
ps->x = temp;
// End of sequence that writes to *p
// but leaves bit pattern unchanged
return ps->x;
}
If p
and q
identify the same storage with no declared type, the way clang and gcc interpret the ->
operator, this code will set the effective type of the storage to struct s
and then to struct t
. It will then use type struct t
to read the storage (perfectly legitimate). Although it will then use type s
to write the storage twice, both clang and gcc will recognize that the two writes, taken together, leave the storage holding the same bit pattern as it held before the first write. Because the two writes, taken together, do not modify the storage, they do not alter the Effective Type of the storage, and behavior of the code would be undefined in the scenario where p
and q
are equal.
unsigned get_float_bits(float *fp) { return *(unsigned*)fp; }
must recognize the possibility that the function might observe the stored representation of afloat
? A compiler which treated character types like all others, but made even a modest effort to acknowledge places where cross-type pointer derivation was obvious, could perform more useful optimizations, while being compatible with more programs, than one that will willfully blind to cross-type derivation. – Shick