703: Raw Pointer Arithmetic
Difficulty: 4 Level: Expert Navigate memory with `ptr.add()`, `ptr.sub()`, and `ptr.offset()` โ and wrap the results safely.The Problem This Solves
Safe Rust slices are bounds-checked at every indexed access. That's the right default, but it generates redundant checks in loops where you've already verified the range once at entry. More critically, some patterns don't map to slice indexing at all: strided access over every Nth element, in-place reversal via two converging pointers, or reading a struct field at a known byte offset in a memory-mapped buffer. Raw pointer arithmetic gives you the same power as C pointer arithmetic โ advance a pointer by N elements, swap two elements by address, walk a buffer without re-indexing โ with one difference: Rust requires you to put the arithmetic in an `unsafe` block and document why each offset stays in bounds. The compiler trusts you; reviewers verify you. unsafe is a tool, not a crutch โ use only when safe Rust genuinely can't express the pattern.The Intuition
A raw pointer is a typed address. `ptr.add(n)` computes `ptr + n size_of::<T>()` โ it advances by N elements*, not N bytes. `ptr.offset(n)` is the signed version: positive advances forward, negative steps back. `ptr.sub(n)` is `ptr.add(wrapping_neg(n))`. The compiler cannot see that `ptr.add(offset)` stays within the original allocation. You must carry that proof in your head (or your `// SAFETY:` comment). The contract: the resulting pointer must point within the same allocated object, or one-past-the-end (for comparison only, not dereference).How It Works in Rust
/// Collect every `stride`-th element without repeated bounds checks.
pub fn strided_collect(slice: &[i32], stride: usize) -> Vec<i32> {
if slice.is_empty() || stride == 0 { return vec![]; }
let mut result = Vec::new();
let base: *const i32 = slice.as_ptr();
let len = slice.len();
let mut offset = 0usize;
while offset < len {
result.push(unsafe {
// SAFETY: offset < len == slice.len(); base is valid for len
// elements; alignment guaranteed by slice invariant.
*base.add(offset)
});
offset = offset.saturating_add(stride);
}
result
}
/// In-place reversal via two converging raw pointers.
pub fn ptr_reverse(slice: &mut [i32]) {
let len = slice.len();
if len < 2 { return; }
let base: *mut i32 = slice.as_mut_ptr();
let (mut lo, mut hi) = (0usize, len - 1);
while lo < hi {
unsafe {
// SAFETY: lo < hi < len; both offsets are in bounds; lo != hi
// so the two pointers never alias the same slot.
std::ptr::swap(base.add(lo), base.add(hi));
}
lo += 1; hi -= 1;
}
}
Key rules: always compute offsets from a slice's `as_ptr()` (which guarantees alignment), always check bounds before the `unsafe` block, and never create two `*mut` pointers to the same element simultaneously.
What This Unlocks
- SIMD and vectorisation โ feed contiguous raw pointers to `std::arch` intrinsics that process 4/8/16 elements at a time.
- Memory-mapped file parsing โ walk a `*const u8` over a mmap'd buffer, reading fixed-size records at computed offsets without copying.
- Custom data structures โ implement a ring buffer, deque, or gap buffer where advancing the write head wraps around with pointer arithmetic.
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Pointer arithmetic | `Bigarray` indexing or `Bytes.get` | `ptr.add(n)` / `ptr.sub(n)` in `unsafe` |
| Bounds checking | Always on (or `unsafe_get` convention) | Safe slices always check; raw pointers never check |
| Signed offset | Not a language concept | `ptr.offset(n: isize)` โ negative steps backwards |
| Two-pointer swap | `Array` swap function | `std::ptr::swap(a, b)` โ takes two raw pointers |
| Stride access | Manual index arithmetic | `ptr.add(offset)` with manual stride tracking |
//! 703 โ Raw Pointer Arithmetic
//! ptr.add(), ptr.sub(), ptr.offset() with safe wrappers.
/// Collect every `stride`-th element using raw pointer arithmetic.
pub fn strided_collect(slice: &[i32], stride: usize) -> Vec<i32> {
if slice.is_empty() || stride == 0 {
return vec![];
}
let mut result = Vec::new();
let base: *const i32 = slice.as_ptr();
let len = slice.len();
let mut offset = 0usize;
while offset < len {
result.push(unsafe {
// SAFETY: offset < len == slice.len(); base is valid for len elements;
// alignment guaranteed by slice invariant.
*base.add(offset)
});
offset = offset.saturating_add(stride);
}
result
}
/// Reverse a slice in-place via raw pointer arithmetic (swap lo/hi).
pub fn ptr_reverse(slice: &mut [i32]) {
let len = slice.len();
if len < 2 { return; }
let base: *mut i32 = slice.as_mut_ptr();
let mut lo = 0usize;
let mut hi = len - 1;
while lo < hi {
unsafe {
// SAFETY: lo < hi < len; both in bounds; we never alias same slot
// because lo != hi.
let a = base.add(lo);
let b = base.add(hi);
std::ptr::swap(a, b);
}
lo += 1;
hi -= 1;
}
}
/// Demonstrate ptr.offset() with signed arithmetic.
pub fn offset_demo(slice: &[i32]) -> Option<i32> {
if slice.len() < 3 { return None; }
let mid: *const i32 = unsafe { slice.as_ptr().add(slice.len() / 2) };
Some(unsafe {
// SAFETY: mid is within the slice; mid.offset(-1) is also in the slice
// because len >= 3 guarantees len/2 >= 1.
*mid.offset(-1)
})
}
fn main() {
let data: Vec<i32> = (0..10).collect();
println!("Every other: {:?}", strided_collect(&data, 2));
println!("Every third: {:?}", strided_collect(&data, 3));
let mut rev = data.clone();
ptr_reverse(&mut rev);
println!("Reversed: {:?}", rev);
println!("offset_demo: {:?}", offset_demo(&data));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stride_1() {
assert_eq!(strided_collect(&[1, 2, 3, 4, 5], 1), vec![1, 2, 3, 4, 5]);
}
#[test]
fn test_stride_2() {
assert_eq!(strided_collect(&[0, 1, 2, 3, 4, 5, 6], 2), vec![0, 2, 4, 6]);
}
#[test]
fn test_ptr_reverse() {
let mut v = vec![1, 2, 3, 4, 5];
ptr_reverse(&mut v);
assert_eq!(v, [5, 4, 3, 2, 1]);
}
#[test]
fn test_ptr_reverse_even() {
let mut v = vec![1, 2, 3, 4];
ptr_reverse(&mut v);
assert_eq!(v, [4, 3, 2, 1]);
}
#[test]
fn test_offset_demo() {
let s = [10, 20, 30, 40, 50];
assert_eq!(offset_demo(&s), Some(20)); // mid=2, mid-1=1 => 20
}
}
(* OCaml: pointer arithmetic is hidden; arrays use safe index operations.
We simulate stride-based access with functional list building. *)
(** Read every stride-th element starting at start. *)
let strided_read (arr : 'a array) ~(start : int) ~(stride : int) : 'a list =
let n = Array.length arr in
let rec go i acc =
if i >= n then List.rev acc
else go (i + stride) (arr.(i) :: acc)
in
go start []
let () =
let data = [| 0; 1; 2; 3; 4; 5; 6; 7; 8; 9 |] in
let every_other = strided_read data ~start:0 ~stride:2 in
print_string "Every other: ";
List.iter (fun x -> Printf.printf "%d " x) every_other;
print_newline ();
let every_third = strided_read data ~start:0 ~stride:3 in
print_string "Every third: ";
List.iter (fun x -> Printf.printf "%d " x) every_third;
print_newline ()