5.1.32 · D5C Programming

Question bank — restrict keyword — aliasing hint

2,321 words11 min readBack to topic

Before we start, a one-line refresher of the vocabulary every question leans on:

The picture behind every question

Aliasing is a statement about byte ranges on the memory line. The first figure is the whole topic in one image — keep it in your head while answering.

Figure — restrict keyword — aliasing hint

The second figure shows why the promise pays off: it turns a strict per-element chain into a reorderable block.

Figure — restrict keyword — aliasing hint

The third figure shows the parameter-reassignment nuance (6.7.3.1 p6): the promise latches onto the value on entry, so overwriting the parameter first can void it.

Figure — restrict keyword — aliasing hint

True or false — justify

Each is a statement. Decide true/false and give the reason — the reason is the whole point. Where code differs, picture the two byte ranges of the previous figure.

restrict guarantees the compiler produces faster code.
False — restrict only relaxes the alias-analysis result the compiler feeds to its scheduler/vectorizer; it does not add any transform. Alias analysis normally returns "may-alias" for two int* (a store to a[i] may-alias the load of b[i]), which forbids reordering; restrict downgrades that to "no-alias" (C99 §6.7.3.1 p4). If the loop had no cross-iteration store→load hazard to begin with, the generated code is byte-identical. Speed is a consequence, never a promise.
restrict and const do the same job with different spellings.
False — const (see const qualifier) is a modifiability qualifier: "no write through this pointer." restrict is an aliasing qualifier: "no other, non-derived path reaches this memory" (§6.7.3.1 p4). They are orthogonal; const int * restrict p asserts both at once.
If you write restrict and it's a lie, you'll get a compiler error.
False — §6.7.3.1 places the burden entirely on the programmer; the compiler is permitted to assume the promise and is not required to diagnose a violation. Precise whole-program aliasing is undecidable, so it silently trusts you — the lie compiles clean and becomes UB, often only visible once -O2/-O3 alias-analysis actually uses the fact.
Marking factor as restrict lets the compiler load *factor once, given the full declaration below.
True — declaration is void scale(float * restrict v, float * restrict factor, int n){ for(int i=0;i<n;i++) v[i] *= *factor; }. Without restrict factor, alias analysis must assume factor may point inside v[0..n), so the store v[i] = ... may-alias *factor, forcing a re-load of *factor each iteration. restrict factor proves factor's range is disjoint from v's, so *factor is loop-invariant and hoisted into a register.
memcpy is safe on overlapping regions just like memmove.
False — the C standard prototypes memcpy(void * restrict s1, const void * restrict s2, size_t n); those restricts make overlap UB. memmove uses no restrict and its spec explicitly permits overlap. See memcpy vs memmove.
Two restrict pointers into genuinely disjoint arrays honour the promise even if their addresses are numerically close.
True — the promise is about shared bytes, i.e. whether the ranges [p, p+len) and [q, q+len) intersect (§6.7.3.1 p4 speaks of the same object). Numerically adjacent but non-overlapping ranges share no byte, so no aliasing.
Passing the same array through two restrict parameters is legal as long as you never write through either.
True but fragile — if every access is a read, no write→read hazard exists, so §6.7.3.1 p4 (which is triggered by a modification through one path) is never violated. But one write through one parameter plus any access through the other = UB. Safest never to alias.
restrict changes the type of the pointer.
True — it is a type qualifier (§6.7.3.1 p1), so int * restrict is a distinct qualified type; but it changes only the aliasing contract, not the pointer's size or representation.
A restrict written on a non-pointer declaration is a syntax error.
False — §6.7.3.1 p1 permits restrict syntactically on other types but says it has no effect. E.g. int restrict x; is not the intent (compilers usually warn/reject it in that exact spot), but where the grammar allows a qualifier on a non-pointer, it is simply ignored. The rule to remember: restrict only means something on a pointer-to-object.

Spot the error

Each snippet has a conceptual flaw (or a deliberate non-flaw). Name it and explain why it bites, tracing the byte ranges from the first figure.

void copy(int * restrict d, const int * restrict s, int n){ for(int i=0;i<n;i++) d[i]=s[i]; }
copy(a+1, a, 3);
``` ::: Error: `d = a+1` covers bytes `[a+1, a+4)` and `s = a` covers `[a, a+3)` — they overlap on `a+1,a+2`. The `restrict` promise is a lie → UB. With vectorization the result is unpredictable (`{a0,a0,a1,a2}` or worse). Use `memmove`.
 
```c
void f(int * restrict p){ int *q = malloc(4); *q = *p; free(q); }
``` ::: **No error.** At the machine model, `malloc` returns storage whose lifetime *begins* with this call and which has no other live pointer (the standard says the returned object is *disjoint from every other object*, C99 §7.20.3). So `q`'s range cannot equal or overlap `p`'s range — `p` remains the sole path to *its* object. `restrict` forbids other pointers to the **same** memory, not the mere existence of other pointers to *different* memory.
 
```c
void g(int * restrict p, int * restrict q){ int *r = p; *r = 5; *q = *r; }
``` ::: Conditional error: `r` is *based on* `p` (§6.7.3.1 p4), so `*r = 5` is a write through `p`'s path. If at runtime `q` reaches the same object, the "`p` and `q` disjoint" promise is broken → UB. Deriving `r` from `p` is always fine; aliasing `q` with `p`'s object is the violation.
 
```c
int x=0; int * restrict p=&x; int * restrict s=&x; *p=1; return *s;
``` ::: Error: `p` and `s` are two **non-derived** `restrict` pointers to the *same* object `x`; we modify through `p` then read through `s`. That is exactly the write-through-one, access-through-another pattern §6.7.3.1 p4 forbids → UB.
 
```c
void h(float * restrict v, int n){ float t=*v; for(int i=0;i<n;i++) v[i]+=v[0]; }
``` ::: **No error.** `v[0]` and `v[i]` are both accessed through pointers *based on* `v`, so they are the *same* path in the eyes of §6.7.3.1 p4. The promise only bans *other, non-derived* pointers. A `restrict` pointer overlapping *itself* is always allowed.
 
```c
void k(int * restrict p, int n){ p = p + 1; *p = 0; }
``` ::: **No error, but the nuance matters**6.7.3.1 p6): for a parameter, the promise is anchored to the pointer's value *on entry*. Reassigning `p` here still leaves every access *based on* that entry value, so the promise stands. Contrast the next section's edge case where reassigning to an **unrelated** pointer voids it.
 
---
 
## Why questions
 
Why must a non-`restrict` compiler assume `a` might overlap `b` in `a[i]=b[i]+c[i]`? ::: Because C's aliasing model lets any `int*` point at any `int` object; the compiler's alias analysis therefore returns "may-alias" for `a` vs `b`/`c` and must defend against a store to `a[i]` changing a future `b`/`c` load. See [[Compiler optimization & vectorization (SIMD)]].
 
Why does the aliasing assumption specifically block vectorization? ::: SIMD processes several elements *at once and out of order* (second figure). That is only legal if a store in the batch cannot invalidate a load in the same batch. May-alias forces the strict per-element load→add→store chain, killing the block form.
 
Why is `restrict` "the programmer's job" rather than the compiler's? ::: Proving two arbitrary pointers never alias across a whole program is undecidable, so §6.7.3.1 offloads the guarantee to the human who knows the call sites, and lets alias analysis *assume* it.
 
Why does breaking the promise sometimes "work" at `-O0` but fail at `-O2`? ::: `-O0` barely uses alias facts, so it re-reads memory naturally and may accidentally be correct; `-O2` *acts on* the no-alias fact (hoisting, reordering, vectorizing), exposing the lie as wrong output.
 
Why is copying a `restrict` pointer into a local not a way to "escape" the promise? ::: The copy is *based on* the original (§6.7.3.1 p4), so accesses through it still count as accesses through the original path. The promise follows the *data lineage*, not the variable name.
 
Why does `restrict` say nothing about thread safety or `volatile`? ::: It is a single-threaded, per-object *aliasing* contract only. Concurrency and hardware-visible side effects are governed by memory models and `volatile`, which are separate mechanisms.
 
Why can a multi-level pointer make `restrict` confusing? ::: In `int * restrict * p`, the `restrict` qualifies the *inner* pointer type (the `int*` that `*p` yields), not `p` itself; the promise is about the objects those inner pointers reach, so you must track *which* level carries the qualifier before reasoning about disjointness.
 
---
 
## Edge cases
 
What does `restrict` mean when `n == 0` and the loop never runs? ::: Nothing is accessed, so §6.7.3.1 p4 is never triggered; the promise is trivially satisfied even if the ranges overlap. The pointers may even be equal.
 
Is `T * restrict p = NULL;` legal? ::: Yes to declare — a null `restrict` pointer is fine as long as you never *dereference* it. The promise constrains actual accesses, and a null pointer accesses nothing.
 
If two `restrict` pointers are equal but you only ever *read* through both, is it UB? ::: No — §6.7.3.1 p4 is only violated when a *modification* through one path coexists with any access through a different, non-derived path. All-reads never modifies, so it is safe.
 
Does `restrict` on a parameter constrain memory *after* the function returns? ::: No — §6.7.3.1 p6 ties the promise to the function body (the pointer's block). Once the call ends, other pointers may freely reach that memory.
 
If you reassign a `restrict` **parameter** to an unrelated pointer before the first access, does the promise still bind the new target? ::: Careful — the promise is anchored to the value *on entry*6.7.3.1 p6, third figure). Accesses through a pointer *not based on* the entry value are outside the promise, so you can *void* the intended guarantee; conversely accesses through the entry value (and things based on it) remain bound. Rule of thumb: don't reassign restrict parameters if you want the guarantee.
 
What if a `restrict` pointer's region is one byte adjacent to another's, `q = p + n` exactly? ::: Legal — `p` covers `[p, p+n)`, `q` starts at `p+n`; the ranges touch but share no byte. Disjoint means no shared byte, and there is none.
 
Can a single `restrict` pointer overlap *itself*, e.g. reading `v[0]` while writing `v[i]`? ::: Yes — both accesses are *based on* the same `restrict` pointer, so they are the same path and §6.7.3.1 p4 does not apply. Self-overlap is always fine.
 
Two `restrict` pointers to different **members of the same struct** — is that aliasing? ::: No — distinct members occupy distinct, non-overlapping bytes of the struct, so pointers to `&s.a` and `&s.b` reach different objects and honour the promise, even though they live in one struct. (A pointer to the *whole* struct, however, would overlap both.)
 
Does `restrict` on a non-pointer type do anything? ::: No — §6.7.3.1 p1 says it is *allowed but has no effect* on non-pointer types. Only a pointer-to-object carries the aliasing meaning; anywhere else it is silently a no-op.
 
---
 
> [!recall]- One-line summary of the traps
> Almost every trap is one of three lies: (1) confusing `restrict` with `const` (modifiability vs aliasing), (2) thinking the compiler *checks* it (§6.7.3.1 puts it on you), or (3) forgetting the promise covers **based-on** pointers as the *same* path, while only a *different, non-derived* path plus a write causes UB.
 
## Connections
- [[Pointers in C]]
- [[const qualifier]]
- [[Pointer aliasing]]
- [[memcpy vs memmove]]
- [[Compiler optimization & vectorization (SIMD)]]
- [[Undefined behaviour in C]]
- [[C99 standard features]]