Skip to content

Vec

Level: 101 → 201 · for newcomers

One line: A Vec<T> is three numbers on the stack — pointer, length, capacity — and one allocation on the heap that it grows by doubling.

fn main() {
    let mut scores: Vec<u32> = Vec::new();
    scores.push(5);
    scores.push(3);
    scores.push(0);
    println!("{scores:?} len {} cap {}", scores.len(), scores.capacity());
    // [5, 3, 0] len 3 cap 4
}

vec![5, 3, 0] is the same thing in one line, and (1..=100).collect() is the same thing when the values come from somewhere.

Three numbers, and none of them is the data

size_of::<Vec<T>>() is 24 on a 64-bit machine whatever T is — a Vec<[u8; 999]> is also 24 bytes. The elements are not in the Vec; the Vec is a receipt for them.

let mut scores: Vec<u32> = Vec::new();
scores.push(5); scores.push(3); scores.push(0);

        stack                          heap
  ┌───────────┬────┐
  │ ptr       │  ●─┼──────────▶ ┌───┬───┬───┬───┐
  │ len       │  3 │            │ 5 │ 3 │ 0 │   │
  │ capacity  │  4 │            └───┴───┴───┴───┘
  └───────────┴────┘            └─── len ───┘  ↑ bought, not yet filled

Three pushes, and the fourth slot is already paid for: len counts the three, capacity counts the four. A String is the same picture with u8 cells, and a slice &[T] is the picture minus capacity — a pointer and a length, no ownership, no room to grow.

field says
pointer where the elements are
len how many are initialised
capacity how many fit before the next allocation

len is what you almost always want. capacity matters exactly once: when you are about to fill it.

One type per Vec, and the compiler works out which

A Vec<T> is homogeneous, exactly as an array is: one T, chosen once, for every element. Which T is a question of inference rather than a rule about position.

fn main() {
    let inferred = vec![1, 2, 3];            // Vec<i32> — nothing said otherwise
    let suffixed = vec![1, 2, 3u64];         // Vec<u64> — the LAST element decided
    let annotated: Vec<f64> = vec![1.0];     // Vec<f64> — the annotation decided
    // let mixed = vec![1, "two", 3.0];      // error[E0308]: expected integer, found `&str`
    println!("{} {} {}", inferred[2], suffixed[2], annotated[0]);   // 3 3 1
}

"The first element sets the type" is the usual summary and vec![1, 2, 3u64] disproves it — unsuffixed integer literals are still open when the third one arrives and closes them. What is true is that there is exactly one answer, and rustc stops at the first element that disagrees with it:

error[E0308]: mismatched types
 --> mixed.rs:2:25
  |
2 |     let mixed = vec![1, "two", 3.0];
  |                         ^^^^^ expected integer, found `&str`

One error, naming the second element. The 3.0 is never reached and never mentioned, so fixing the &str earns you a second compile and a second error.

Two ways to hold a mixture, and they are the same trick — give every element one type:

when element type
an enum the set of possibilities is closed and you wrote it Vec<Value>
a boxed trait object the set is open — a plugin registry, a value chosen at run time Vec<Box<dyn Display>>

The Box is not decoration. T must be Sized — an implicit bound nothing writes down — so Vec<dyn Display> is error[E0277], and the box is the layer of indirection that gives every element the one known size a Vec lays out its buffer with. Same bound behind Vec<str> and Vec<[T]> being rejected.

Growth is amortised doubling, and you can watch it

Nine pushes into a Vec::new() cause three reallocations — capacity goes 0 → 4 → 8 → 16 — and each one copies everything already stored into the new buffer. Doubling is what makes pushing n items cost O(n) in total instead of O(n²); the exact sequence is this std's choice, not a promise in the language.

If you know the size, say so:

fn main() {
    let mut sized: Vec<u32> = Vec::with_capacity(9);
    for n in 1..=9 { sized.push(n); }
    println!("cap {}", sized.capacity());   // 9
}

One allocation instead of three, and nothing copied. collect() does this for you when the iterator knows its own length — a range does, a filter does not.

What growable costs

Nothing is free, and it is worth knowing which line of your program pays.

[f64; 4] Vec<f64> of 4
bytes 32, all payload 24 header + 32 heap = 56
in how many places one two
to construct nothing an allocator call
to reach element i one memory access load the pointer, then the element
to push impossible compare len against capacity, and occasionally reallocate

Read the last two rows together, because the second is the one people over-weight. A hundred pushes into a Vec::new() is a hundred capacity comparisons and six allocator calls — the bookkeeping is paid at the edges, on construction, growth and drop, not per element. The pointer hop is real and the optimizer hoists it out of any loop that does not reallocate, which is why the cost almost never shows up where a newcomer expects it and why "use a Vec" survives as the default advice. Array or Vec? is the whole trade, including the four things the array buys back.

The move is the clearest picture of what the header actually is:

fn main() {
    let wave: Vec<f64> = vec![0.0, 0.707, 1.0, 0.707];
    let elements_were = wave.as_ptr();
    let moved = wave;                                  // a move, not a copy
    println!("{}", moved.as_ptr() == elements_were);   // true
}

Twenty-four bytes changed hands and not one f64 was touched. Moving an array of the same four values copies all thirty-two — which is the same fact from the other side, and the reason the array is Copy and the Vec is not.

When the growing is over: Box<[T]>

capacity is there to make the next push cheap. A Vec that has finished being built still pays for it — 8 bytes of header, plus whatever slots the doubling bought and nothing filled. into_boxed_slice ends both: it hands the spare slots back to the allocator and drops the field, leaving a Box<[T]> of pointer and length. StringBox<str> is the same trade for text.

Cloudflare published the arithmetic in August 2026. Its 1.1.1.1 resolver holds over 250 billion DNS cache entries at a time and never modifies an entry once written, so a byte per entry is 250 GB of fleet memory and the third number was pure overhead:

Vec and String fields per cache entry 8
header bytes returned, at 8 per field 64
heap slots the doubling had reserved returned as well
across 250 billion entries over 15 TB

That was one of five changes, which together took an entry from 953 bytes to 420 and freed roughly 100 TB. The conversion is one-way by design: a Box<[T]> cannot grow, so it is what you do to a buffer you have finished filling, not the type you fill.

It derefs to a slice, so slice methods just work

Everything on arrays and slices applies here: first, last, contains, sort, windows, iter, and indexing that panics while .get returns Option — each with a page of its own in the slice methods reference. That is also the argument for &[T] over &Vec<T> in a signature — a &Vec<u32> coerces to &[u32] at the call site, so taking the slice costs the caller nothing and accepts three more kinds of argument.

Written out, the coercion is invisible enough to miss:

fn main() {
    let v: Vec<f64> = vec![0.0, 0.707, 1.0, 0.707];
    let a: [f64; 4] = [0.0, -0.707, -1.0, -0.707];
    let sv: &[f64] = &v;   // slice of a Vec   — borrowed from the heap buffer
    let sa: &[f64] = &a;   // slice of an array — borrowed from the stack
    println!("{} {}", sv.len(), sa.len());   // 4 4
}

Two owners with nothing in common — one heap allocation with a growth policy, one inline block of thirty-two bytes — and &[f64] is the same 16-byte pointer-and-length for both. Both annotations are load-bearing, which is the part that is easy to miss: drop them and &v is a &Vec<f64>, &a is a &[f64; 4], and neither is a slice. The & never chose — the let is a coercion site and the written type is what fires it, which is the same coercion as_slice gives a name to. &[T] is therefore the type to write in a signature, and the reason arrays and slices is a lesson Vec sends you to rather than repeating.

Removing: the one that keeps order, and the one that is fast

fn main() {
    let mut a: Vec<char> = "abcdef".chars().collect();
    let mut b = a.clone();
    a.remove(1);        // ['a', 'c', 'd', 'e', 'f']  — everything after shifts down
    b.swap_remove(1);   // ['a', 'f', 'c', 'd', 'e']  — the last element fills the hole
    println!("{a:?} {b:?}");
}

O(n) against O(1). swap_remove is the right answer surprisingly often — but if the Vec is a ranking, it has silently reordered your results.

To delete many, retain is one pass with one shift per survivor:

fn main() {
    let mut scores = vec![5, 3, 0, 4, 0, 2];
    scores.retain(|&s| s > 0);
    println!("{scores:?}");   // [5, 3, 4, 2]
}

The trap: deleting inside an index loop

fn main() {
    let mut v = vec![5u32, 0, 3, 0, 4];
    let mut i = 0;
    while i < v.len() {
        if v[i] == 0 { v.remove(i); } else { i += 1; }   // note: no i += 1 after a removal
    }
    println!("{v:?}");   // [5, 3, 4]
}

Increment i after the remove and every element following a deleted one is skipped — so [0, 0] leaves one zero behind. Nothing warns, because every index used is valid. Writing for i in 0..v.len() is worse still: the bound is computed once, and the loop then indexes past the shrinking end.

Rust does stop you doing this while iterating: for x in &v { v.remove(…) } is a borrow-check error, not a run-time surprise. The index loop is the version that escapes that check, which is exactly why retain exists.

If you are coming from another language

  • Python. Vec is list, closely: push/append, pop/pop, remove(i)/del xs[i], retain/a comprehension. The differences are the ones type and ownership bring. A Vec<T> holds one type, so [1, "two", 3.0] has no direct translation — and the reason Python's list does not need one is worth carrying across: every CPython list slot is already a pointer to a boxed object, so the list is heterogeneous the way Vec<Box<dyn Any>> is, not the way a hypothetical Vec<Anything> would be. Rust makes you write the box and name what the boxed things have in common. xs[i] panics rather than raising something you can catch, and .get(i) is the version that returns Option; and v2 = v1 moves where Python aliases, so the double-mutation bug that Python's shared reference causes is a compile error. The one habit to unlearn is for i in range(len(xs)) — Rust's for x in &v is both faster and impossible to get wrong, and the index form is where the deletion trap above lives. Python's list.pop(0) is O(n) for the same reason Vec::remove(0) is; VecDeque is Rust's collections.deque.
  • ABAP. A Vec<T> is a STANDARD TABLE OF ty and the correspondence is close enough to be useful: APPEND is push, DELETE itab INDEX i is remove, READ TABLE … INDEX is get, LOOP AT is for x in &v. Three things transfer directly. A STANDARD TABLE OF ty is homogeneous for exactly the reason a Vec<T> is — one row type, declared once — so the discipline needs no unlearning, and ABAP's answer for a mixed column is the same as Rust's: a structure with a type field, which is an enum wearing a different name. DELETE itab WHERE cond is retain with the condition negated — one statement, one pass, and the same reason to prefer it over deleting in a loop. And the ABAP rule that you must not DELETE from the table you are looping over is Rust's borrow checker, enforced by convention there and by the compiler here. What ABAP has that Vec does not is the sorted and hashed table kinds with their key declarations; in Rust those are separate types — BTreeMap and HashMap — rather than a property of the table.
  • C++. std::vector exactly, down to the growth strategy and reserve being with_capacity. swap_remove is the idiom C++ programmers write by hand as std::swap(v[i], v.back()); v.pop_back();. Iterator invalidation is the same hazard and Rust turns it into a compile error.
  • Java / C#. ArrayList<T> / List<T>, with ensureCapacity / the capacity constructor. ConcurrentModificationException is thrown at run time for the case Rust rejects at compile time.

The verified output

Verified output of the_vec.rs — regenerated by tools/run_examples.py, never hand-typed.

1. Three numbers: pointer, length, capacity
   size_of::<Vec<u32>>()  = 24  (three usize)
   size_of::<Vec<[u8; 999]>>() = 24  — the same three, whatever T is
   The elements are not in the Vec. The Vec is a receipt for them.

2. One type per Vec — and the compiler works out which
   vec![1, 2, 3]           -> Vec<i32>
   vec![1, 2, 3u64]        -> Vec<u64>   — the LAST element decided
   let a: Vec<f64> = ...   -> Vec<f64>   — the annotation decided
   `vec![1, "two", 3.0]` is error[E0308], and it names the second
   element: one type per Vec, and rustc stops at the first quarrel.
   Two ways to hold a mixture, both of which give every element
   ONE type:
   enum, closed set:   Int(7), Text("seven"), Real(7.5)
   Box<dyn>, open set: 7, seven, 7.5

3. Growth is amortised doubling, and you can watch it
   Vec::new()            len 0 cap 0
   push(1) reallocated   len 1 cap 0 -> 4
   push(5) reallocated   len 5 cap 4 -> 8
   push(9) reallocated   len 9 cap 8 -> 16
   after 9 pushes        len 9 cap 16
   Nine pushes, three allocations. Doubling is why pushing n items
   costs O(n) in total rather than O(n^2), and the exact sequence is
   this std's choice, not a promise in the language.

4. If you know the size, say so
   with_capacity(9): cap 9 -> 9 — no reallocation at all
   Same nine values, one allocation instead of three, and no copying
   of the old contents.

5. What `growable` costs
   [f64; 4]        32 bytes, all payload, in one place
   Vec<f64> of 4   24 header + 32 heap = 56 bytes, in two
   moving it copied the 24-byte header and not one element: true
   100 pushes = 100 capacity checks and 6 allocator calls
   So the bookkeeping is paid at the EDGES — allocate, grow, free —
   and per element it is one pointer hop the optimizer hoists out of
   a loop. Which is why `use a Vec` is still the default advice.

6. A Vec derefs to a slice, so slice methods just work
   total(&v) = 45 — `total` takes &[u32] and was handed a &Vec<u32>
   v.first() = Some(1), v.contains(&5) = true
   v.iter().rev().take(3): [9, 8, 7]
   &Vec<f64>   as &[f64]: [0.0, 0.707, 1.0, 0.707]
   &[f64; 4]   as &[f64]: [0.0, -0.707, -1.0, -0.707]
   Two owners, two storage stories, ONE borrowed type: 16 bytes of
   pointer-and-length from the heap, 16 from the stack — the same
   type, so the same function takes either.
   Write &[T] in a signature and both callers work. Write &Vec<T>
   and you have refused arrays, slices and everything borrowed.

7. Removing: the one that keeps order, and the one that is fast
   remove(1)      -> b, left ['a', 'c', 'd', 'e', 'f']   (everything after shifts down)
   swap_remove(1) -> b, left ['a', 'f', 'c', 'd', 'e']   (the last element fills the hole)
   O(n) versus O(1). If you are about to sort anyway, take the O(1).
   retain(|s| s > 0) on [5, 3, 0, 4, 0, 2] -> [5, 3, 4, 2]
   One pass, one shift per survivor — not one `remove` per zero.

Practice

Count the reallocations, then delete them. Push a hundred numbers into a Vec::new(), and count how many times the capacity changed and how many elements were copied in total — measure it in the program, from len() just before each growth, rather than working it out on paper. Then do the same with Vec::with_capacity(100) and with (1..=100).collect().

Two questions the numbers raise. truncate(3) on the hundred-element Vec — what does capacity become, and why is that the behaviour you want? And remove(1) versus swap_remove(1) on a list of names: both are one line and one is a bug, so say which and under what assumption about the caller.

Solution

the_vec_kata.rs in full — pasted here by tools/run_examples.py from the file CI compiles and runs.

//! Kata solution: count the reallocations, then delete them.
//!
//!   rustc --edition 2024 the_vec_kata.rs -o /tmp/vk && /tmp/vk

/// Reads a tally into a fresh Vec the naive way, reporting every reallocation.
fn grow_naively(rows: &[u32]) -> (Vec<u32>, usize, usize) {
    let mut out = Vec::new();
    let mut reallocations = 0;
    let mut copied = 0;
    for &r in rows {
        let before = out.capacity();
        let filled = out.len();
        out.push(r);
        if out.capacity() != before {
            reallocations += 1;
            copied += filled;   // everything already stored is moved to the new buffer
        }
    }
    (out, reallocations, copied)
}

/// The same, told how many rows are coming.
fn grow_sized(rows: &[u32]) -> (Vec<u32>, usize, usize) {
    let mut out = Vec::with_capacity(rows.len());
    let mut reallocations = 0;
    let mut copied = 0;
    for &r in rows {
        let before = out.capacity();
        let filled = out.len();
        out.push(r);
        if out.capacity() != before {
            reallocations += 1;
            copied += filled;
        }
    }
    (out, reallocations, copied)
}

fn main() {
    let rows: Vec<u32> = (1..=100).collect();

    println!("1. One hundred pushes");
    let (naive, n1, copied1) = grow_naively(&rows);
    let (sized, n2, copied2) = grow_sized(&rows);
    println!("   Vec::new()          -> {} items, {n1} reallocations, cap {}", naive.len(), naive.capacity());
    println!("   with_capacity(100)  -> {} items, {n2} reallocations, cap {}", sized.len(), sized.capacity());
    println!("   Every reallocation copies everything already stored: {copied1} u32s");
    println!("   moved, against {copied2} for the sized version. The counting is in");
    println!("   the program — `out.len()` just before each growth, summed.");

    println!();
    println!("2. collect() already knew");
    let collected: Vec<u32> = (1..=100).collect();
    println!("   (1..=100).collect() -> {} items, cap {}", collected.len(), collected.capacity());
    println!("   A range knows its own length, so collect asked once and got it");
    println!("   right. That is `size_hint`, and it is why collect usually beats");
    println!("   a hand-written push loop without you doing anything.");

    println!();
    println!("3. Shrinking does not give the memory back");
    let mut v = collected.clone();
    v.truncate(3);
    println!("   after truncate(3): len {} cap {}", v.len(), v.capacity());
    v.shrink_to_fit();
    println!("   after shrink_to_fit(): len {} cap {}", v.len(), v.capacity());
    println!("   `clear` and `truncate` drop the elements and keep the buffer —");
    println!("   which is the behaviour you want in a loop that refills it.");

    println!();
    println!("4. Two removals, one of which is a bug");
    let names = ["Ada", "Ben", "Cara", "Dan", "Eve"];
    let mut keep_order: Vec<&str> = names.to_vec();
    let mut fast: Vec<&str> = names.to_vec();
    keep_order.remove(1);
    fast.swap_remove(1);
    println!("   remove(1)      -> {keep_order:?}");
    println!("   swap_remove(1) -> {fast:?}");
    println!("   Identical cost story, opposite guarantees. If this Vec is a");
    println!("   ranking, swap_remove has silently reordered the results.");

    println!();
    println!("5. Deleting while iterating, the three ways");
    let start = vec![5u32, 0, 3, 0, 4];
    let mut by_retain = start.clone();
    by_retain.retain(|&s| s != 0);
    let by_filter: Vec<u32> = start.iter().copied().filter(|&s| s != 0).collect();
    let mut by_index = start.clone();
    let mut i = 0;
    while i < by_index.len() {
        if by_index[i] == 0 {
            by_index.remove(i);
        } else {
            i += 1;
        }
    }
    println!("   retain            -> {by_retain:?}   in place, one pass");
    println!("   filter().collect  -> {by_filter:?}   a new Vec, borrows the old");
    println!("   index loop        -> {by_index:?}   correct only because `i` is");
    println!("   not incremented after a removal — the classic off-by-one is to");
    println!("   write a `for i in 0..len` here and skip every element after a");
    println!("   deleted one. Rust will not stop you: the indices are all valid.");
}

Verified output of the_vec_kata.rs — regenerated by tools/run_examples.py, never hand-typed.

1. One hundred pushes
   Vec::new()          -> 100 items, 6 reallocations, cap 128
   with_capacity(100)  -> 100 items, 0 reallocations, cap 100
   Every reallocation copies everything already stored: 124 u32s
   moved, against 0 for the sized version. The counting is in
   the program — `out.len()` just before each growth, summed.

2. collect() already knew
   (1..=100).collect() -> 100 items, cap 100
   A range knows its own length, so collect asked once and got it
   right. That is `size_hint`, and it is why collect usually beats
   a hand-written push loop without you doing anything.

3. Shrinking does not give the memory back
   after truncate(3): len 3 cap 100
   after shrink_to_fit(): len 3 cap 3
   `clear` and `truncate` drop the elements and keep the buffer —
   which is the behaviour you want in a loop that refills it.

4. Two removals, one of which is a bug
   remove(1)      -> ["Ada", "Cara", "Dan", "Eve"]
   swap_remove(1) -> ["Ada", "Eve", "Cara", "Dan"]
   Identical cost story, opposite guarantees. If this Vec is a
   ranking, swap_remove has silently reordered the results.

5. Deleting while iterating, the three ways
   retain            -> [5, 3, 4]   in place, one pass
   filter().collect  -> [5, 3, 4]   a new Vec, borrows the old
   index loop        -> [5, 3, 4]   correct only because `i` is
   not incremented after a removal — the classic off-by-one is to
   write a `for i in 0..len` here and skip every element after a
   deleted one. Rust will not stop you: the indices are all valid.

See also

  • What a Vec guarantees — the small print under this page: why the pointer is never null but need not point anywhere, why capacity() can be relied on to the element, and the three things std refuses to promise
  • Vec methods — one page per method, with a compiled example each: the reference this lesson is the introduction to
  • Arrays and slices — the type Vec derefs to, and where its methods actually live
  • Array or Vec? — the same two types from the deciding end: what the array buys, and the four cases where the length really is a fact about the problem
  • Grids and nested Vecs — what vec![vec![0; w]; h] allocates, and the flatter thing most grids should be
  • Box — one value on the heap, where Vec is many
  • Stack and heap — what the pointer points at, and what it costs to follow
  • Ownership and moves — why let v2 = v1; leaves v1 unusable
  • iter, iter_mut, into_iter — the three doors onto a Vec, and which one consumes it
  • Collect the iterator into a Vec — the other way one of these gets built, and the six questions that never needed it
  • Building a String — the same capacity story, for text
  • bytearray is the mutable one ↗ — Python's Vec<u8>, and the design question this page never has to ask: with no mut on a Python name, mutability has to be recorded in the type, so Python needs two byte types where Rust needs one Vec and a keyword

Sources

Std library types: Vectors ↗ in Rust by Example, and std::vec::Vec, whose Capacity and reallocation section is the authority for everything this page says about growth.

The cache numbers are from How we saved 100 terabytes of memory by optimizing 1.1.1.1’s DNS cache ↗ — Sebastiaan Neuteboom, Cloudflare, 27 August 2026, whose other four changes are u16 section offsets in place of two more pointers, an Optioned owner name, boxing only an enum's large variants, and finally storing the records as raw wire-format bytes.

Po polsku

Vec<T> to trzy liczby na stosie — wskaźnik, długość i pojemność — plus jedna alokacja na stercie. Warto trzymać rozdzielone dwa słowa, które po polsku łatwo się zlewają: długość (length) to ile elementów faktycznie jest, a pojemność (capacity) to ile się zmieści, zanim trzeba będzie przealokować. Rosną niezależnie i tylko pierwsza z nich mówi coś o twoich danych.

Rośnie przez podwajanie, i to podwajanie jest powodem, dla którego dopisywanie na koniec uchodzi za tanie, choć czasem kopiuje całą zawartość. Po polsku mówi się o koszcie zamortyzowanym: pojedyncze push bywa drogie, ale średnia po wielu wywołaniach jest stała. Kto zna ArrayList z Javy albo list z Pythona, zna dokładnie ten sam mechanizm pod inną nazwą — a std::vector z C++ to wręcz ten sam typ, łącznie z nazwą.

Dwie rzeczy przydatne od pierwszego dnia. Vec dereferencuje się do wycinka (&[T]), więc wszystkie metody wycinka działają na nim bez żadnej konwersji — dlatego szukając „metody Vec", trafia się połowę odpowiedzi w dokumentacji slice i to nie pomyłka. I pułapka, ta sama co przy unieważnianiu iteratorów: usuwanie wewnątrz pętli po indeksach przesuwa resztę w lewo i przeskakuje element; remove zachowuje kolejność i jest liniowe, swap_remove jest stałoczasowe i kolejność niszczy, a gdy chodzi o odsianie wielu elementów, właściwą odpowiedzią jest retain.

Szukaj po polsku: wektor w Ruscie · długość a pojemność · koszt zamortyzowany · rust Vec capacity · rust swap_remove vs remove