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
E
is said to be based on objectP
[whereP
is a restrict-qualified pointer object] if (at some sequence point in the execution ofB
[whereB
is the block associated with the declaration ofP
] prior to the evaluation ofE
) modifyingP
to 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,
E
depends on the value ofP
itself rather than on the value of an object referenced indirectly throughP
. For example, if identifierp
has type(int **restrict)
, then the pointer expressionsp
andp+1
are based on the restricted pointer object designated byp
, but the pointer expressions*p
andp[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
, letL
be any lvalue that has&L
based onP
. IfL
is used to access the value of the objectX
that it designates, andX
is also modified (by any means), then the following requirements apply:T
[whereT
is the type to whichP
is declared to point] shall not be const-qualified. Every other lvalue used to access the value ofX
shall also have its address based onP
. Every access that modifiesX
shall be considered also to modifyP
, for the purposes of this subclause. IfP
is assigned the value of a pointer expressionE
that is based on another restricted pointer objectP2
, associated with blockB2
, then either the execution ofB2
shall begin before the execution ofB
, or the execution ofB2
shall 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
,q
is "based on"p
(because ifp
was modified at sequence pointS
to point to a copy ofx
, the conditionalp != p0
would become true, changing the value ofq
). - In the expression
*p = 1
,p
is "based on"q
(because ifq
was modified at sequence pointS
to point to a copy ofx
, the conditionalq != q0
would 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"p
and "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
restrict
violation, 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.