Two reasons this optimization isn't legal for an unknown f(const int *restrict p) when the caller() can only see a prototype:
- The
restrict stuff only applies to objects that were actually accessed via the restrict pointer. f could ignore its arg (or access p[1] but not p[0]) and modify the caller's object via a global pointer. CPPreference omits this part of the ISO C standard's definition.
f can legally cast away a const qualifier and modify the object unless the underlying object is const, so the optimization isn't valid even if there can't be any other pointer to the object floating around.
In general it's legal to cast away const, as long as the pointed-to object itself wasn't defined as const. The optimization you're suggesting would break with this f in this program which I think is free of UB:
void f(const unsigned *restrict p) {
unsigned *unqualified_p = (unsigned*)p;
++*unqualified_p; // *restrict p still exists but this pointer was derived from p
}
unsigned global;
int main(){ // aka caller(int *p)
unsigned n = global;
f(&global);
return n == global;
}
This compiles cleanly with clang or gcc -O2 -Wall -Wextra. Godbolt. (And runs cleanly with -fsanitize=undefined, although I wouldn't be confident of UBsan catching const or restrict cast violations if I'm wrong and this isn't well-defined.)
Example of just the const part being enough to defeat optimization, with escape analysis giving the compiler everything you were hoping it could infer from restrict:
void g(const int *p);
int bar(){
int a = 1; // non-escaped local, no global pointer to it can exist
g(&a);
return a == 1; // actually compares, not returning a constant
}
Optimization wouldn't be valid in this case. Unfortunately this demo isn't as good as I'd hoped because GCC and clang miss a valid optimization for passing a pointer to a const int local variable where the callee can't change it, because the actual object itself is const. A g that casted away const and modified the object would be UB. (If the initializer is a compile-time constant, compilers do constant propagation even across a call that passes the address.) (Godbolt).
int baz(int x){
const int a = x, b = x; // non-escaped locals, like the function arg
// and the underlying objects are const so it would be UB for g to change them
g(&a);
return a == b; // GCC and clang still compare!
// return x == b; // does optimize out the compare, neither local escapes
}
int barc(){
const int a = 1;
g(&a);
return a == 1; // constant folding to return 1
}
restrict only kicks in on access.
https://en.cppreference.com/w/c/language/restrict says if some object that is accessible through P (directly or indirectly) is modified, by any means, then all accesses to that object (both reads and writes) in that block must occur through P (directly or indirectly) [otherwise UB].
But the requirement isn't just that the object is accessible, it's that it actually was accessed. (Thanks @Eric Postpischil for catching this.) From the C 2018 standard (n2310), 6.7.3.1 Formal definition of restrict
[definitions of B as a block, P as a restrict pointer, etc.]
During each execution of B, let L be any lvalue that has &L based on P. If L is used to access the
value of the object X that it designates, and X is also modified (by any means), then the following
requirements apply:
T shall not be const-qualified. Every other lvalue used to access the value of
X shall also have its address based on P. Every access that modifies X shall be considered also to
modify P, for the purposes of this subclause. If P is assigned the value of a pointer expression E that
is based on another restricted pointer object P2, associated with block B2, then either the execution
of B2 shall begin before the execution of B, or the execution of B2 shall end prior to the assignment.
If these requirements are not met, then the behavior is undefined.
Here an execution of B means that portion of the execution of the program that would correspond to
the lifetime of an object with scalar type and automatic storage duration associated with B.
[later stuff is all just examples, no further clauses that impose any restrictions on behaviour.]
Everything is conditional on the value actually being accessed via a restrict pointer. This is confirmed earlier in 6.7.3 with "An object that is accessed through a restrict-qualified pointer has a special association with that
pointer..."
int *global_ptr;
// No UB even if called with p == global_ptr
void f1(const int *restrict p) {
*global_ptr = 1; // which happens to point at the same object as p
}
// also legal with p == global_ptr
void f2(const int *restrict p) {
p[1]++;
*global_ptr = 1; // points at p[0]
}
The CPPreference page about C restrict appears to also have a mistake in its discussion of const, in the function parameter section. They talk about the possibility of f taking float const * restrict b and what that implies, but it's written as if casting away const wasn't legal. (Unless I'm misreading what they're saying and they meant with the actual definition visible, not just a prototype.)
int f(const int *p) __attribute__((pure));(would be[[reproducible]]in the upcoming C23 standard). However, herefis not pure (as it prints tostdout). Hopefullyrestrictis a way to say that the function is pure with respect to that particular pointer.