Pointer Arithmetic
Pointer arithmetic computes a new memory address by advancing or retreating a raw pointer (*const T or *mut T) by a number of elements (size_of::<T>() bytes per step) or raw bytes. Unlike C, where + and - apply directly to pointers, Rust uses explicit methods—.add(count), .sub(count), .offset(isize), .byte_add(bytes), and .wrapping_add(count)—to distinguish in-bounds allocation arithmetic from wrapping address math.
Element Stride vs. Byte Stride
Standard pointer arithmetic methods (.add(n), .sub(n), .offset(n)) scale n by size_of::<T>() bytes:
$$\text{address}\big(\text{ptr.add}(n)\big) = \text{address}(\text{ptr}) + n \times \text{size_of}::\langle T \rangle()$$
Starting in Rust 1.75, .byte_add(bytes), .byte_sub(bytes), and .byte_offset(bytes) advance a typed pointer *const T by an exact number of bytes without requiring a cast to *const u8 and back.
Array `[u32; 4]` (size_of::<u32>() == 4 bytes):
+----------+----------+----------+----------+----------+
| arr[0] | arr[1] | arr[2] | arr[3] | one-past |
+----------+----------+----------+----------+----------+
^ ^ ^ ^
base base.add(1) base.add(3) base.add(4) (valid to compute, UB to deref!)
(0x1000) (0x1004) (0x100C) (0x1010)
In-Bounds (add / offset) vs. Wrapping (wrapping_add / wrapping_offset)
| Method Family | unsafe to Call? |
Safety Contract |
|---|---|---|
.add(usize), .sub(usize), .offset(isize) |
Yes (unsafe fn) |
Both the starting pointer and the resulting pointer must stay within the bounds of the same allocated object (or one element past the end), and the byte offset must not overflow isize::MAX. |
.byte_add(usize), .byte_sub(usize), .byte_offset(isize) |
Yes (unsafe fn) |
Same in-bounds and one-past-the-end rules as .add(), measured in bytes. |
.wrapping_add(usize), .wrapping_sub(usize), .wrapping_offset(isize) |
No (safe fn) |
Performs wrapping two's-complement address arithmetic without asserting in-bounds LLVM getelementptr inbounds rules (dereferencing the result still requires unsafe and a valid target). |
.offset_from(origin) |
Yes (unsafe fn) |
Both pointers must point into (or one-past-the-end of) the same allocation, and the distance must be an exact multiple of size_of::<T>(). |
why Computing Out-of-Bounds with .add() Is Immediate UB
Even if you never dereference the resulting pointer, calling ptr.add(n) that steps more than one element past the end of an allocation (or one element before the start) is immediate undefined behavior because .add() lowers to LLVM's getelementptr inbounds instruction. Notice that pointing exactly one element past the end (ptr.add(slice.len())) is explicitly allowed so C-style ptr != end_ptr iteration loops work soundly:
fn sum_with_raw_pointers(slice: &[u64]) -> u64 {
let mut current: *const u64 = slice.as_ptr();
// SAFETY: Computing one-past-the-end of a valid slice allocation is always sound.
let end: *const u64 = unsafe { current.add(slice.len()) };
let mut total = 0;
while current != end {
// SAFETY: `current` is strictly less than `end`, so it points to an
// initialized element inside `slice`.
unsafe {
total += *current;
current = current.add(1);
}
}
total
}
fn main() {
let numbers = [10_u64, 20, 30, 40];
let total = sum_with_raw_pointers(&numbers);
// Compute element distance between `&numbers[3]` and `&numbers[0]`:
let p0: *const u64 = &raw const numbers[0];
let p3: *const u64 = &raw const numbers[3];
// SAFETY: `p0` and `p3` belong to the same `numbers` array allocation.
let distance: isize = unsafe { p3.offset_from(p0) };
println!("total: {}, distance (p3 - p0): {} elements", total, distance);
}
total: 100, distance (p3 - p0): 3 elements