The parent note told you restrict is a promise : "the memory behind this pointer is reached only through this pointer." A promise is either kept (regions disjoint → legal, fast) or broken (regions overlap → undefined behaviour). Every worked example below is one concrete arrangement of pointers in memory, and we ask the same two questions each time : do the regions overlap? and is the restrict promise therefore kept or broken?
This page is the exhaustive drill. First we lay out a matrix of every kind of overlap situation C can hand you; then we solve one example per cell.
See the parent for the theory: restrict — aliasing hint .
Before any example, two terms and one abbreviation — because the whole page hangs on telling them apart.
Definition Touch vs. Overlap vs. UB
UB is short for undefined behaviour : the C standard places no requirement whatsoever on what the program does. It may compute the "expected" thing, garbage, or differ between optimization levels. We write UB everywhere below.
Two byte-ranges overlap when they share at least one byte . Overlap is the thing that breaks a restrict promise.
Two byte-ranges touch (are adjacent ) when one ends exactly where the next begins, sharing no byte . Touching is safe — it is not overlap.
Definition How we write a range: inclusive vs. half-open
Two equivalent notations appear on this page — learn both because C code mixes them:
Inclusive arr[0..3] means the four elements at indices 0, 1, 2, 3 — both ends included .
Half-open [0, 4) means "start at 0, stop before 4" — the square bracket [ includes the left end, the round bracket ) excludes the right end. So [0, 4) is also indices 0, 1, 2, 3.
They describe the same four cells. Half-open is handy for overlap tests because a range starting at s covering n items is exactly [s, s+n), and two half-open ranges [a, b) and [c, d) are disjoint exactly when b ≤ c (or d ≤ a).
Several examples below say a result "differs between a scalar build and a vectorized build." Let's earn those two words before we use them.
Definition Scalar build vs. vectorized (SIMD) build
A scalar build is the plain, literal machine code: it processes the loop one element at a time , in source order (e.g. dst[0], then dst[1], ...). This is roughly what you get at -O0 (no optimization).
A vectorized build is what an optimizing compiler (at -O2/-O3) produces when it is allowed to process several elements at once . Modern CPUs have wide registers that hold, say, 4 or 8 ints side by side; one machine instruction adds/copies the whole batch. This is called SIMD (Single Instruction, Multiple Data) — see Compiler optimization & vectorization (SIMD) .
Why it matters here. To batch a block, the compiler often loads all the source elements first , then stores the whole batch — an order that is only valid if source and destination don't overlap. restrict is precisely the promise that unlocks this reordering. So:
When the promise is kept , scalar and vectorized builds agree → one deterministic answer.
When the promise is broken (overlap), the two builds can read stale-vs-fresh data and produce different results — which is exactly why overlap is UB.
Whenever an example shows "scalar gives X, vectorized gives Y, and X ≠ Y", that disagreement is the proof of undefined behaviour.
Now the picture. Two arrays live in a flat strip of memory. Each pointer names a start address and (through how many elements you touch) a range of bytes.
Figure s01 (described). A horizontal strip of eight numbered memory cells (indices 0–7). A black arrow beneath spans cells 2–5 labelled "pointer p range"; a black arrow above spans cells 0–3 labelled "pointer q range". The two cells they both cover (2 and 3) are outlined in red and labelled "shared bytes (danger zone)". The red outline is the overlap.
If the red zone is empty, the promise can be kept. If it has even one byte, the promise is broken the moment both pointers touch it.
Here is every case-class this topic can throw at you:
Cell
Situation
Regions overlap?
restrict legal?
A
Two separate arrays, different objects
No
✅ Yes
B
Forward-shifted copy dst = src+k, k>0
Yes (partial)
❌ No
C
Backward-shifted copy dst = src-k, k>0
Yes (partial)
❌ No
D
Exact same pointer dst == src, n>0
Fully (a self-alias)
❌ No
E
Adjacent, touching but not overlapping (dst = src+n)
No (edge case!)
✅ Yes
F
Scalar pointer possibly inside an array (factor in v)
Depends
✅ only if outside
G
Zero-length / degenerate (n = 0)
Vacuously none
✅ Yes (no accesses)
H
Real-world word problem (image row blur)
Must be checked
depends
I
Exam twist: const + restrict alias, overlap only visible at -O3
Yes (full)
❌ No
We now walk every cell, in table order A → I — one worked example each.
Examples 1–4 call two tiny functions. Here are their signatures so nothing is used undefined:
Worked example Example 1 — Cell A: two separate arrays (promise kept)
int b [ 4 ] = { 10 , 20 , 30 , 40 };
int c [ 4 ] = { 1 , 2 , 3 , 4 };
int a [ 4 ];
add (a, b, c, 4 );
Forecast: Do a, b, c overlap? What is a afterward?
List the ranges. a, b, c are three independent array objects. Each occupies its own bytes; C guarantees distinct objects don't overlap.
Why this step? The whole promise is about byte ranges — so we always start by locating them.
Check the red zone. Empty for every pair. Promise kept .
Why this step? Empty danger zone = restrict is truthful = compiler may vectorize freely.
Compute. a[i] = b[i] + c[i] → a = {11, 22, 33, 44}.
Why this step? Since no aliasing, plain elementwise addition; the answer is deterministic.
Verify: 11+22+33+44 = 110; and b[0]+c[0] = 10+1 = 11. Deterministic, correct. ✅
Worked example Example 2 — Cell B: forward-shifted copy (promise broken)
int arr [ 5 ] = { 1 , 2 , 3 , 4 , 5 };
copy (arr + 1 , arr, 4 ); // dst = arr+1, src = arr, n = 4
Forecast: Where do the ranges overlap, and why is the result garbage?
Figure s02 (described). The five cells of arr (values 1–5). A black arrow spans indices 0–3 labelled "src = arr[0..3]"; a red arrow spans indices 1–4 labelled "dst = arr[1..4]". The three shared cells 1–3 are outlined red and labelled "overlap arr[1..3] → UB".
Locate ranges (half-open). src = [0,4), dst = [1,5). Their intersection [1,4) = indices 1,2,3 is non-empty. Red zone non-empty ⇒ they overlap .
Why this step? Overlap ⇒ the restrict promise on copy is a lie ⇒ UB.
Trace the "honest" left-to-right (scalar) copy to see the danger. dst[0]=src[0]→arr[1]=1; now arr={1,1,3,4,5}. dst[1]=src[1] reads arr[1] which is now 1 → arr[2]=1 → {1,1,1,4,5}; dst[2]=src[2] reads arr[2]=1 → arr[3]=1 → {1,1,1,1,5}; dst[3]=src[3] reads arr[3]=1 → arr[4]=1 → {1,1,1,1,1}.
Why this step? One element at a time, left-to-right, each write corrupts a not-yet-read source cell.
State the real answer: UB. A vectorized (SIMD) build first loads the whole source block {1,2,3,4} into registers, then stores it into dst, giving {1,1,2,3,4} — different again. There is no single correct output .
Why this step? UB means we cannot even predict it — the lesson is "don't do this." Use memmove.
Verify: the scalar trace gives {1,1,1,1,1}; the vectorized/SIMD trace gives {1,1,2,3,4}. Two different results from one call ⇒ genuinely undefined. ✅
Worked example Example 3 — Cell C: backward-shifted copy (still overlap)
int arr [ 5 ] = { 1 , 2 , 3 , 4 , 5 };
copy (arr, arr + 1 , 4 ); // dst = arr, src = arr+1, n = 4
Forecast: Overlap direction is reversed — does a left-to-right copy survive this time?
Locate ranges (half-open). dst = [0,4), src = [1,5). Intersection [1,4) = indices 1,2,3 — again non-empty. Promise broken .
Why this step? Overlap is symmetric; direction doesn't remove the shared bytes.
Trace left-to-right (scalar). arr[0]=arr[1]=2→{2,2,3,4,5}; arr[1]=arr[2]=3→{2,3,3,4,5}; arr[2]=arr[3]=4; arr[3]=arr[4]=5 → {2,3,4,5,5}.
Why this step? Here left-to-right happens to give the "shifted" result — but it's still UB because you promised no overlap.
Punchline. The scalar answer looks right, yet a vectorized (SIMD) build may read a stale block and differ. Legality does not depend on luck.
Why this step? Cells B and C together prove: overlap is illegal regardless of which side is bigger.
Verify: scalar result {2,3,4,5,5}; sum 2+3+4+5+5 = 19. That the answer seems fine here is exactly the trap. Still UB. ✅
Worked example Example 4 — Cell D: exact self-alias
dst == src, n>0 (promise broken)
int arr [ 3 ] = { 7 , 8 , 9 };
copy (arr, arr, 3 ); // dst == src, n = 3 -> full overlap
Forecast: You're copying an array onto itself . Values won't change — so is it safe?
Locate ranges (half-open). dst = [0,3) and src = [0,3) are the same range. Their intersection is the whole thing — maximal overlap .
Why this step? dst and src are two distinct restrict parameters that name the same bytes. Each is a separate access path to those bytes ⇒ the "reached only through this pointer" promise is violated for both.
Why "values unchanged" does NOT rescue it. Even though dst[i] = src[i] writes each cell's own value back, the promise is about pointer disjointness, not about whether the bytes end up equal. The moment the compiler assumes dst and src never alias, it is entitled to reorder or fuse the loads/stores in ways only valid under disjointness. Assumption false ⇒ UB.
Why this step? Separates "the data happens to be equal" from "the contract is kept" — they are unrelated.
Contrast with Cell G. Change n to 0 and the loop never runs → zero accesses → nothing to violate (Example 7). So the danger of Cell D lives entirely in n>0.
Why this step? Pins down exactly which input makes the self-alias illegal.
Verify: the values are numerically unchanged — arr still holds {7,8,9} — yet the call is still UB because two restrict parameters alias the same region. "Same numbers out" ≠ "legal". ✅
Worked example Example 5 — Cell E: adjacent but NOT overlapping (edge case, promise kept)
int arr [ 8 ] = { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 };
copy (arr + 4 , arr, 4 ); // dst = arr[4..7], src = arr[0..3]
Forecast: They touch at the boundary between index 3 and 4 — is that overlap?
Figure s03 (described). Eight cells of arr. A black arrow spans indices 0–3 labelled "src = arr[0..3]"; another black arrow spans indices 4–7 labelled "dst = arr[4..7]". A red vertical line marks the seam between cell 3 and cell 4, labelled "seam: src_end = dst_start, NO shared byte → legal".
Range test at the seam (half-open). src = [0,4), dst = [4,8). Disjointness test: src ends at 4, dst starts at 4, and 4 ≤ 4 holds ⇒ disjoint . They touch but do not overlap.
Why this step? This is exactly the "touch ≠ overlap" definition in action.
Conclude promise kept. dst = src + n exactly is the boundary case; it is legal. The disjointness test is src_end ≤ dst_start — a non-strict (≤) inequality, so equality at the seam still counts as disjoint.
Why this step? The classic exam distractor: because ≤ (not <) is allowed, the touching case is safe.
Compute. arr[4..7] becomes {1,2,3,4} → whole array {1,2,3,4,1,2,3,4}.
Why this step? Disjoint ⇒ deterministic copy.
Verify: result {1,2,3,4,1,2,3,4}; sum = 2*(1+2+3+4) = 20. ✅
Worked example Example 6 — Cell F: scalar pointer inside vs outside the array
void scale ( float * restrict v , const float * restrict factor , int n );
float x [ 3 ] = { 2.0 f , 4.0 f , 6.0 f };
float k = 10.0 f ;
scale (x, & k , 3 ); // factor points OUTSIDE x -> legal
scale (x, & x [ 0 ], 3 ); // factor aliases x[0] -> UB!
Forecast: Both calls compile. Which one keeps the promise, and what does each want to compute?
Case F-legal: factor = &k. k is a separate object, not inside x. Promise kept. Compiler loads *factor = 10 once into a register. x = {20, 40, 60}.
Why this step? This is precisely the register-caching win the parent described.
Case F-illegal: factor = &x[0]. Now factor points into v. Writing v[0] changes *factor. Overlap ⇒ UB.
Why this step? The promise "factor doesn't point inside v" is now false.
Show why the illegal one is ambiguous. Honest per-iteration reload (scalar): x[0]=2*2=4; then *factor is 4, so x[1]=4*4=16, x[2]=6*4=24 → {4,16,24}. But with restrict the compiler cached *factor=2, giving x = {4, 8, 12}. Two answers ⇒ UB.
Why this step? Concretely demonstrates the "load once vs reload" divergence.
Verify (legal call): {2,4,6} * 10 = {20,40,60}, sum 120. Verify (illegal): cached path {4,8,12} vs reload path {4,16,24} differ ⇒ UB confirmed. ✅
Worked example Example 7 — Cell G: degenerate zero-length (vacuously legal)
int arr [ 3 ] = { 7 , 8 , 9 };
copy (arr, arr, 0 ); // dst == src, but n = 0
Forecast: dst and src are the same address — surely that overlaps?
Count the accesses. The loop for(i=0;i<0;i++) runs zero times. No load, no store, no byte is ever touched.
Why this step? The promise is about bytes accessed through the pointer. Zero accesses ⇒ nothing to violate.
Conclude vacuously legal. With n = 0 both ranges are [0,0) — empty; empty ranges cannot overlap.
Why this step? Degenerate inputs must be covered — the empty-loop case is always safe, even with identical pointers.
Contrast with Cell D at n>0. copy(arr, arr, 3) (Example 4) does access shared bytes ⇒ UB. Only n=0 neutralizes the self-alias.
Why this step? Separates the safe empty case from the unsafe self-copy case.
Verify: after copy(arr,arr,0) array unchanged {7,8,9}, sum 24. ✅
Worked example Example 8 — Cell H: real-world word problem (image row averaging)
Statement. You blur an image row in place by replacing each pixel with the average of itself and its right neighbour: out[i] = (in[i] + in[i+1]) / 2. A junior codes it with restrict and out == in. One row: {100, 200, 40, 80} (last pixel left unchanged).
Forecast: With out and in being the same buffer, is the restrict version valid, and what row appears?
Identify overlap. out == in ⇒ full overlap (like Cell D) ⇒ restrict promise broken ⇒ UB.
Why this step? Same buffer is the maximal overlap; this is the number-one real bug in image code.
Trace the honest in-place left-to-right (scalar) computation (what a non -restrict build might do): out[0]=(100+200)/2=150; now in={150,200,40,80}; out[1]=(200+40)/2=120; out[2]=(40+80)/2=60; in[3] untouched =80 → {150,120,60,80}.
Why this step? Shows the intended-but-fragile result.
State the fix. Either (a) drop restrict and accept sequential semantics, or (b) write to a separate output buffer (Cell A) so restrict is honest and vectorization is legal.
Why this step? Correctness first: only promise disjointness when it's actually true.
Verify: with separate buffers the deterministic averages are (100+200)/2=150, (200+40)/2=120, (40+80)/2=60, last =80 → {150,120,60,80}, sum 410. ✅
Worked example Example 9 — Cell I: exam twist (
const + restrict, and the -O3 surprise)
Statement. Explain the output of:
int sum_twice ( const int * restrict p , int * restrict q , int n ) {
* q = 0 ;
for ( int i = 0 ; i < n; i ++ ) * q += p [i]; // accumulate into *q
return * q;
}
int data [ 3 ] = { 5 , 5 , 5 };
int total = sum_twice (data, & data [ 0 ], 3 ); // q aliases p[0]!
Forecast: p is const (read-only through p), q is writable. Does const save us from the aliasing bug?
Separate the two qualifiers. const promises you won't modify through p . restrict promises no other pointer overlaps p . They are independent (see const qualifier ).
Why this step? The exam relies on students conflating them.
Spot the overlap. q = &data[0] is inside the region p reads — full overlap on that byte. Writing *q mutates p[0]. Even though p is const, the memory is changed — through q, which is legal for q but violates p's restrict (something other than p touched p's bytes). ⇒ overlaps ⇒ not legal ⇒ UB.
Why this step? const restricts the verb through p , not the underlying bytes.
Predict the split. Honest reload each iteration (scalar / -O0): *q starts 0; +=p[0] but p[0] was just set to 0 by *q=0 ⇒ 0; +=p[1]=5⇒5; +=p[2]=5⇒10; returns 10 . But at -O3 (vectorized), the compiler trusts restrict and may cache the original p[0..2]={5,5,5} in registers before the loop ⇒ 5+5+5=15. Two answers ⇒ UB.
Why this step? This is the infamous "works at -O0, breaks at -O3" bug the parent warned about — a direct consequence of the scalar-vs-vectorized split defined above.
Verify: scalar/-O0 path returns 0+5+5 = 10; cached/-O3 path returns 5+5+5 = 15. 10 ≠ 15 ⇒ undefined behaviour proven. Fix: don't alias, or drop restrict. ✅
Recall
Every cell, one line each.
Cell A (two arrays) legal? ::: Yes — disjoint, deterministic, vectorizable.
Cell B (dst=src+1) legal? ::: No — partial overlap [1,4), UB.
Cell C (dst=src-1) legal? ::: No — still overlaps; a "correct-looking" scalar result is luck.
Cell D (dst==src, n>0) legal? ::: No — full self-alias; two restrict params name the same bytes ⇒ UB, even though the values come out unchanged.
Cell E (dst=src+n) legal? ::: Yes — adjacent, no shared byte (src_end ≤ dst_start, non-strict).
Cell F (scalar inside array) legal? ::: Only if the scalar lives outside the array.
Cell G (n=0) legal? ::: Yes — zero accesses can't violate the promise, even if dstsrc.
Cell H (in-place blur, out in) legal? ::: No — full overlap; use a separate output buffer.
Cell I (const+restrict alias) legal? ::: No — const doesn't stop the aliasing; -O0 and -O3 disagree ⇒ UB.
"Touch ≠ Overlap." Ranges that meet at a boundary (Cell E) are fine; ranges that share a byte (B, C, D, H, I) are UB. Draw the strip, mark the red zone, then decide.
Pointers in C
Pointer aliasing
const qualifier
memcpy vs memmove
Compiler optimization & vectorization (SIMD)
Undefined behaviour in C
C99 standard features