Skip to content

← Unsafe and Soundness step 7 of 24

Medium Primitives

Pointer arithmetic: offset, add, sub, wrapping_*

Find the maximum value in each consecutive window of a vector using raw pointers.

pub fn windows_max(v: Vec<i64>, w: usize) -> Vec<i64>

If w == 0 or w > v.len(), return an empty vector. Otherwise return v.len() - w + 1 maxima, ordered by each window's starting position. For w == 1, return the input values; for w == v.len() on nonempty input, return one maximum. Empty input always produces an empty output.

v = [1, 3, 2, 5, 4], w = 2 -> [3, 3, 5, 5]
v = [-9, -2, -7],    w = 2 -> [-2, -2]

Values may include i64::MIN and i64::MAX. Only compare values; do not perform arithmetic on them. Repeated maxima remain repeated in the output.

Use raw-pointer traversal, a pointer to the end of the initialized elements, and offset_from_unsigned to count the elements remaining. Do not use indexing or slice::windows in your solution. These are exercise requirements; output tests alone do not enforce the implementation approach.

Distinguish initialized elements from allocated capacity

v.as_ptr() points to the vector's first element. For a nonempty vector, start.add(v.len()) marks the end of its initialized elements. Use this as the loop sentinel and do not read from it.

The vector may have spare capacity beyond its length. In that case, this sentinel is still inside the allocation, rather than one past the allocation. Spare capacity does not contain additional initialized i64 values that the algorithm may read.

This distinction affects where a bad traversal first violates a rule. Reading past len is invalid even if the pointer arithmetic stayed inside allocated capacity. Arithmetic that leaves the allocation's permitted range can itself be undefined behavior, before a dereference occurs.

Choose the arithmetic operation deliberately

Method Unit and purpose
add(n) / sub(n) Move forward/backward by n elements, using a usize.
offset(n) Move by a signed number of elements.
byte_add(n) / byte_offset(n) Move by bytes rather than elements.
offset_from(origin) Compute signed distance in elements.
offset_from_unsigned(origin) Compute a nonnegative distance in elements.
wrapping_add(n) / wrapping_offset(n) Compute a pointer without the same in-bounds arithmetic requirement.

The non-wrapping offset operations are unsafe. For nonzero offsets, their allocation, byte-offset and address-overflow requirements must hold even if you never dereference the result. Within an allocation of exactly ten i64 slots, a pointer to slot zero may advance by ten to the one-past-end sentinel, but not by eleven. The one-past-end pointer is not readable.

wrapping_* operations are safe to compute, but do not grant permission to dereference an invalid address. They preserve the originating pointer's provenance; producing an address numerically equal to another allocation does not make it a pointer that may access that allocation. For this exercise, ordinary add operations with maintained bounds are sufficient.

Maintain a complete-window invariant

Let lo be the start of the candidate window and end the end of the initialized elements. Both derive from the same vector allocation.

  1. Compute end.offset_from_unsigned(lo).
  2. Stop if fewer than w elements remain.
  3. Read exactly w initialized elements from lo, updating the maximum.
  4. Advance lo by one element and repeat.

The distance operation is unsafe too. Here end and lo share an allocation, lo <= end, and their byte distance is an exact multiple of size_of::<i64>() within the permitted range. i64 is not a zero-sized type. An address comparison alone would not establish all of these conditions.

Handle invalid window sizes before starting traversal. In each accepted window, initialize the maximum to its first value or to i64::MIN, not zero.

Repair the starter's two separate problems

Its while lo < end condition visits every initialized starting position, even when too few elements remain for a full window. For w > 1, the trailing iterations read beyond the initialized range. They may also compute pointers beyond the allocation, depending on the width and capacity.

For example, at the last element with w == 2, computing the next pointer may still be permitted, but reading through it is not. It is inaccurate to describe every failure as an out-of-bounds arithmetic operation.

Separately, Clippy flags lo.offset(i as isize) with ptr_offset_with_cast. For this nonnegative element offset, lo.add(i) avoids the unnecessary cast. Fixing that lint does not repair the loop bounds. The lint also applies when w == 1, where the starter's traversal otherwise produces the right windows.

Explain the unsafe blocks

Each read needs an aligned pointer to a live, initialized element. Keep the vector alive and do not mutate or reallocate it while traversing. Each advance must remain within the bounds justified by the current window or sentinel. Each distance calculation must use related pointers and the correct direction.

State these facts in the safety comments rather than writing only “in bounds.” The reference approach checks every element of every window, so its runtime is proportional to (len - w + 1) * w; this task teaches pointer invariants, not the linear-time sliding-maximum algorithm.

A passing submission must compile, pass the tests, and pass clippy -D warnings. Correct outputs do not prove absence of undefined behavior. Tools such as Miri can detect many violations on executed paths, but neither a passing test nor a particular observed output establishes safety for all inputs.