Everyone who writes Rust quickly runs into Vec.

When creating a vector with Vec::new() you might expect malloc to be called somewhere in the background. I checked — it isn’t.

I looked into the source to understand why.

Vec::new()

Let’s take a look at the following listing:

fn inspect<T>(v: &Vec<T>) {
    println!("ptr: {:p}", v.as_ptr());
    println!("cap: {}", v.capacity());
    println!("len: {}", v.len());
}

fn main() {
    let v: Vec<i32> = Vec::new();
    inspect(&v);
}

The output we get is:

ptr: 0x4
cap: 0
len: 0

What path did our code take? Let’s debug the Rust runtime!

backtrace

We can see where our function ends up — and the source looks like this: Vec::new_in on GitHub

#[inline]
const fn new_in(alloc: A, align: Alignment) -> Self {
    let ptr = Unique::from_non_null(NonNull::without_provenance(align.as_nonzero_usize()));
    // `cap: 0` means "unallocated". zero-sized types are ignored.
    Self { ptr, cap: ZERO_CAP, alloc }
}

backtrace

The comment alone tells you the principle at work — Zero Cost Abstractions.

When initializing a vector, Rust does it at the lowest possible cost: it sets up a dangling pointer (doc) (ptr: 0x4), but doesn’t actually allocate any memory until it’s needed.

Let’s modify our program, push an element, and see what changes:

fn inspect<T>(v: &Vec<T>) {
    println!("ptr: {:p}", v.as_ptr());
    println!("cap: {}", v.capacity());
    println!("len: {}", v.len());
    println!();
}

fn main() {
    let mut v: Vec<i32> = Vec::new();
    inspect(&v);
    v.push(1);
    inspect(&v);
}

Output:

ptr: 0x4
cap: 0
len: 0

ptr: 0x559967af6d50
cap: 4
len: 1

As you can see, memory was only allocated after push.

Vec::new() is a perfect example of Rust’s philosophy — you only pay for what you use. (zero-cost abstractions)