Vec::as_ptr¶
Level: reference · for working programmers
One line: The raw address of the buffer, without the length.
Stable since 1.37.0. Callable in a const context since 1.87.0.
Reading through it is unsafe, because nothing checks the offset. The elements are contiguous, so ptr.add(i) is element i and consecutive addresses differ by exactly size_of::<T>().
The pointer is invalidated by anything that can reallocate — push, insert, reserve, extend. Whether the address actually changes is the allocator's business: it may grow the block in place or move it, and which you get is not reproducible between runs. There is no answer to test, only a rule to obey — do not hold one across a call that can grow the vector.
Taking the pointer does not borrow the vector for any length of time, so the compiler will not stop you. This is one of the few places Vec asks you to be right without checking.
An empty vector has no allocation and gives a dangling but aligned pointer, not null — valid for zero-length reads only. (Testing it against null is pointless and rustc's useless_ptr_null_checks lint says so.)
The real use is handing a buffer to C: pointer plus length, together. For everything else, a slice carries both and is checked.
Example¶
vec_as_ptr.rs in full — pasted here by tools/run_examples.py from the file CI compiles and runs.
fn main() {
let v = vec![10u32, 20, 30];
let p = v.as_ptr();
// Reading through it is unsafe, because nothing checks the offset.
println!("{} {} {}", unsafe { *p }, unsafe { *p.add(1) }, unsafe { *p.add(2) });
// The elements really are contiguous: consecutive addresses differ by
// exactly size_of::<T>(). Printing the addresses themselves would be a
// different number every run, so derive the gap instead.
let gap = unsafe { p.add(1) as usize - p as usize };
println!("gap between elements: {gap} == size_of::<u32>() {}", size_of::<u32>());
// While there is spare capacity the pointer is stable. Once a push
// reallocates, the old pointer MAY be dangling — the allocator is free
// to grow the block in place or to move it, and which one you get is
// not reproducible. (Measured here: over 400 runs of this program the
// same reallocation moved the buffer sometimes and not others.) That
// "maybe" is exactly why holding a raw pointer across a push is UB:
// there is no answer to check, only a rule to obey.
let mut v: Vec<u8> = Vec::with_capacity(2);
v.push(1);
let before = v.as_ptr() as usize;
v.push(2);
println!("within capacity, pointer stable: {}", v.as_ptr() as usize == before);
let cap_before = v.capacity();
for n in 3..40 { v.push(n); }
println!("capacity {} -> {} — the buffer was reallocated, so `before`",
cap_before, v.capacity());
println!("is not to be trusted whether or not the address changed");
// An empty Vec has no allocation, and gives a dangling but ALIGNED
// pointer rather than null — valid for zero-length reads only.
let e: Vec<u64> = Vec::new();
println!("empty vec pointer is aligned: {}", (e.as_ptr() as usize) % align_of::<u64>() == 0);
// What you almost always want instead: a slice, which carries the length.
let v = vec![1, 2, 3];
let s: &[i32] = &v;
println!("safe equivalent: {:?} len {}", s, s.len());
// The real use is handing a buffer to C: pointer plus length, together.
fn pretend_c_api(ptr: *const u8, len: usize) -> u32 {
(0..len).map(|i| unsafe { *ptr.add(i) } as u32).sum()
}
let bytes = vec![1u8, 2, 3, 4];
println!("sum through a raw pointer: {}", pretend_c_api(bytes.as_ptr(), bytes.len()));
}
Verified output of vec_as_ptr.rs — regenerated by tools/run_examples.py, never hand-typed.
10 20 30
gap between elements: 4 == size_of::<u32>() 4
within capacity, pointer stable: true
capacity 2 -> 64 — the buffer was reallocated, so `before`
is not to be trusted whether or not the address changed
empty vec pointer is aligned: true
safe equivalent: [1, 2, 3] len 3
sum through a raw pointer: 10
See also¶
Vec::as_mut_ptr— the same, writableVec::as_slice— the safe view, which carries the lengthVec::from_raw_parts— rebuilding aVecfrom oneVec::capacity— how far the buffer actually goes