Welcome to Software Development on Codidact!
Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.
Strict aliasing rules and function boundaries
Let's analyze this code, assuming an architecture where the alignment of int64_t
is the same as that of double
:
void
bar(double *f, int64_t *j)
{
*(int64_t *)f = *j;
}
void
foo(void)
{
int64_t i = 1;
bar((double *) &i, &i);
}
- The object is of type
int64_t
. - The pointer is stored as a pointer to a different object type, but that's allowed by C11::6.3.2.3/7: https://port70.net/~nsz/c/c11/n1570.html#6.3.2.3p7
- It's converted back to the actual type of the object, before it's dereferenced.
However, how does this play with restrict
(which I didn't use on purpose) and the strict aliasing rules, and assumptions that compilers may make?
Assuming that bar()
is defined in a different Translation Unit, the compiler will only see the parameters. With that information, it doesn't know that the real type is correctly accessed, so what can it do? Also, AFAIK, compilers interpret this function prototype so that f
and j
can not alias, don't they?
Does this code have Defined Behavior?
To be clear, there are two doubts in that code:
-
Is there an implicit
restrict
ness inbar()
by the fact that the pointers are pointers to incompatible types, even if I didn't use the qualifier? -
Is it valid to convert a pointer back to its real type before dereferencing, or is that information lost in the function boundary?
1 answer
The following users marked this post as Works for me:
User | Comment | Date |
---|---|---|
alx | (no comment) | Jan 20, 2023 at 12:43 |
Assuming that there are no alignment problems between the two pointer types (impl-defined), the code is otherwise well-defined. As per the quoted 6.3.2.3 C allows pretty much any form of wild and crazy pointer conversions by means of a cast, as long as you don't actually de-reference the pointer through the wrong type.
As for the original intention of strict pointer aliasing, it was exactly for cases like this where the compiler should be allowed to assume that double *f
and int64_t *j
do not alias, especially when the caller is in a different translation unit. But since you overrule all such internal decisions with the explicit cast, the compiler just has to fall in line. And it is fine to convert back to the correct pointer type.
restrict
is only an optimizer hint to the compiler and in this case of using the same variable for both arguments, the caller would be violating the "restrict contract" between the caller and a restrict
qualified function. I don't know of any compiler which actually checks for restrict
violations by the caller.
If you would remove the cast and assignment in the function, then the compiler would still be free to assume that the pointers don't alias even without restrict
.
I managed to create a peculiar example with gcc and clang -O3
#include <inttypes.h>
#include <stdio.h>
void
bar(double *f, int64_t * j)
{
int64_t tmp = *j;
*f = 1.0;
if(*j == tmp)
printf("%"PRIi64 " %"PRIi64 "\n", *j, tmp);
}
int main (void)
{
int64_t i = 1;
bar((double *) &i, &i);
printf("%" PRIi64 "\n", i);
}
Output:
1 1
4607182418800017408
Apparently the compiler assumes:
-
tmp
holds the value of*j
- Thus
tmp
and*j
have the same value, since*j
has not been changed. - Thus print
tmp
twice (why load*j
from RAM again?). - Then on the caller side print the actual value of
*j
which may have changed by the function with external linkage.
Without -O3 the 1 1
is never printed.
The same thing happens at -O3 if I change the function to:
void
bar(int64_t*restrict f, int64_t*restrict j)
{
int64_t tmp = *j;
*f = 2;
if(*j == tmp)
printf("%"PRIi64 " %"PRIi64 "\n", *j, tmp);
}
However, if restrict
is dropped it does not print 1 1
regardless of optimizations.
0 comment threads