The following reasonable program seems to have undefined behavior according to the formal definition of restirct in the C standard:
void positive_intcpy(int * restrict q, const int * restrict p, size_t n) {
int *qBgn = q;
const int *pEnd = p + n;
// sequence point S
while (p != pEnd && *p>0) *q++ = *p++;
if (q != qBgn) fprintf(stderr,"Debug: %d.\n",*(q-1)); // undefined behavior!?
}
int main(void) {
int a[6] = {4,3,2,1,0,-1};
int b[3];
positive_intcpy(b,a,3);
return 0;
}
The function copies integers from one array to another, as long as the integers are positive. The fprintf call displays the last positive integer that was copied (if any). There is never any aliasing between p and q.
Is this really UB, or is my reasoning wrong?
This question concerns section 6.7.3.1 of the C99 standard. The relevant text is unchanged in the latest draft for C23.
We are talking about the pointer expression q-1 marked above. Is it based on the restricted pointer object designated by p?
The standard says:
In what follows, a pointer expression
Eis said to be based on objectP[wherePis a restrict-qualified pointer object] if (at some sequence point in the execution ofB[whereBis the block associated with the declaration ofP] prior to the evaluation ofE) modifyingPto point to a copy of the array object into which it formerly pointed would change the value ofE[see footnote]. Note that "based" is defined only for expressions with pointer types.[footnote] In other words,
Edepends on the value ofPitself rather than on the value of an object referenced indirectly throughP. For example, if identifierphas type(int **restrict), then the pointer expressionspandp+1are based on the restricted pointer object designated byp, but the pointer expressions*pandp[1]are not.
In our program, at the sequence point S marked above, modifying p to point to a copy of the a array would cause p != pEnd to always be true (because pEnd is not modified together with p), thus the loop would execute until *p>0 becomes false, thus the value of q at the end of the loop would change (it would be one machine word greater). Therefore we conclude that our expression q-1 is based on p.
Now the standard says:
During each execution of
B, letLbe any lvalue that has&Lbased onP. IfLis used to access the value of the objectXthat it designates, andXis also modified (by any means), then the following requirements apply:T[whereTis the type to whichPis declared to point] shall not be const-qualified. Every other lvalue used to access the value ofXshall also have its address based onP. Every access that modifiesXshall be considered also to modifyP, for the purposes of this subclause. IfPis assigned the value of a pointer expressionEthat is based on another restricted pointer objectP2, associated with blockB2, then either the execution ofB2shall begin before the execution ofB, or the execution ofB2shall end prior to the assignment. If these requirements are not met, then the behavior is undefined.
In our case, L is *(q-1). X is the object at &b[2], which has been modified in B to the value of 2. However, T is const int, which means the program has UB.
You might say that this is harmless, because the compiler will probably not take advantage of such a hard to prove UB case and so won't mis-optimize it.
But the same logic can be applied in reverse, where it becomes truly dangerous:
int f(int *restrict p, int *restrict q) {
int *p0=p, *q0=q;
// sequence point S
if (p != p0) q++;
if (q != q0) p++;
*p = 1;
*q = 2;
return *p + *q;
}
int main(void) {
int x;
printf("%d\n", f(&x,&x));
return 0;
}
GCC does optimize here. GCC -O0 prints 4, while GCC -O3 prints 3.
Unfortunately, reading the standard literally, GCC must be wrong.
Here's why:
- In the expression
*q = 2,qis "based on"p(because ifpwas modified at sequence pointSto point to a copy ofx, the conditionalp != p0would become true, changing the value ofq). - In the expression
*p = 1,pis "based on"q(because ifqwas modified at sequence pointSto point to a copy ofx, the conditionalq != q0would become true, changing the value ofp). - In the body of
f, any access to any object via a pointer expression is always both "based on"pand "based on"q. So no restrict-violation, neither ofrestrict p, nor ofrestrict q. Note that no restrict-qualified pointer is ever assigned a pointer expression insidef(becausep++andq++do not actually happen). - Since there is no
restrictviolation, the program doesn't have UB and the compiler is not allowed to optimize.
It seems to me that the standard failed to define "based on" in the intended way. What is the intended way, then? I mean, clearly there is an intended meaning, which is allowing GCC to optimize, and I believe GCC is morally right in this case. I want to avoid writing programs that might be mis-optimized.
Your reasoning is inconsistent with the intent of the specification.
But this and the reasoning following from it is not what the spec is trying to describe. It isn't interested in differences arising from changes in control flow, such as those you present. It's actually quite hard to describe precisely, so perhaps you can forgive the committee, but here's a go:
consider an execution of the program, and look at all the expression evaluations that have side effects that affect the value of some evaluation of expression
E.imagine modifying the value of
pas described, and performing all those same evaluations again, taking into account the change inp's value and the differences propagating through the results, but not choosing more, fewer, or different expressions to evaluate, nor performing them in a different order.if the ultimate evaluation of
Eyields a different value, thenEis based onp.That's not perfect, I don't think, but I hope it is adequate for explaining the difference between what the spec means to say and what you present.
In that light, I hope you see that the fact that a change to the value of
pat some carefully selected place could cause a different sequence of evaluations to be performed is not germane. In evaluating "based on", we look at effects on the evaluations that were performed, not at how that might have changed the sequence of evaluations.Since
qandq - 1are not, in fact, "based on"pin the sense intended by the spec, the program does not violate the requirements related torestrict-qualified types.... is irrelevant, because it is wrong in the first place.
But let's look at the program in this example:
p,p0,p != p0, andp++are all based onp, but not onq.q,q0,q != q0, andq++are all based onq, but not onp.Either
*por*qcan fill the role of the spec'sL, with the other performing therestrict-violating modification to the one underlying object. The behavior is therefore undefined, and GCC can do whatever it wants with it.