I was reading this blog, and then I found some code that I don't really understand. Why is this bad code?
float *P;
void zero_array() {
int i;
for (i = 0; i < 10000; ++i)
P[i] = 0.0f;
}
int main() {
P = (float*)&P; // cast causes TBAA violation in zero_array.
zero_array();
}
I hope someone could explain it please.
The following code:
float *P;
P = (float*)&P;
P[0] = 0.0f;
violates the strict aliasing rule.
The object P
has effective type float *
, because that is its declared type. C11 6.5/6:
The effective type of an object for an access to its stored value is the declared type of the object, if any.
After the second line has executed, the expression P[0]
denotes the same memory location as P
. (Note: imagine sizeof(float) == sizeof(float *)
for purposes of this explanation. Clearly, if those sizes are different then the situation is even worse!)
Executing P[0] = 0.0f
uses an lvalue of type float
to access an object of type float *
. This violates 6.5/7:
An object shall have its stored value accessed only by an lvalue expression that has one of the following types
In these quotes, "access" means read or write. The list of "following types" does not include any exception that covers using a float
expression to read a float *
This example is a little difficult to get your head around , because it is self-referential. However it is exactly the same principle as this simpler example:
float *Q;
float *P = &Q;
*P = 0.0f;
In this case Q
has type float *
, but it is written via an lvalue of type float
.
This example demonstrates the rationale for the rule. Suppose the strict aliasing rule did not exist, and all aliasing were permitted. Then the evaluation P[0] = 0.0f
changes P
. On a common implementation this would cause P
to now be a null pointer, but we could easily imagine some other value being assigned which leaves P
as a valid pointer to some other variable.
In that case, the line P[1] = 0.0f
must write the other variable successfully. Therefore, the compiler would not be able to replace the loop with a memset
; such a memset would update the original location of P[1]
, but not the new location of P[1]
after P[0] = ....;
had been executed. Optimizations are not allowed to change the observable behaviour of the program.
The existence of the strict aliasing rule means that a compiler can perform optimizations such as changing this loop to a memset, without worrying that the contents of the loop might change the pointer on the fly.
NB. The line P = (float *)&P
may also be an alignment violation. The alignment rule and the strict aliasing rule are completely separate rules and should not be confused with each other. (Some badly-written pages try to 'explain' strict aliasing as some flavour of alignment requirement).