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. String → Box<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.
Vecislist, closely:push/append,pop/pop,remove(i)/del xs[i],retain/a comprehension. The differences are the ones type and ownership bring. AVec<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 wayVec<Box<dyn Any>>is, not the way a hypotheticalVec<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 returnsOption; andv2 = v1moves where Python aliases, so the double-mutation bug that Python's shared reference causes is a compile error. The one habit to unlearn isfor i in range(len(xs))— Rust'sfor x in &vis both faster and impossible to get wrong, and the index form is where the deletion trap above lives. Python'slist.pop(0)is O(n) for the same reasonVec::remove(0)is;VecDequeis Rust'scollections.deque. - ABAP. A
Vec<T>is aSTANDARD TABLE OF tyand the correspondence is close enough to be useful:APPENDispush,DELETE itab INDEX iisremove,READ TABLE … INDEXisget,LOOP ATisfor x in &v. Three things transfer directly. ASTANDARD TABLE OF tyis homogeneous for exactly the reason aVec<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 condisretainwith 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 notDELETEfrom the table you are looping over is Rust's borrow checker, enforced by convention there and by the compiler here. What ABAP has thatVecdoes not is the sorted and hashed table kinds with their key declarations; in Rust those are separate types —BTreeMapandHashMap— rather than a property of the table. - C++.
std::vectorexactly, down to the growth strategy andreservebeingwith_capacity.swap_removeis the idiom C++ programmers write by hand asstd::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>, withensureCapacity/ the capacity constructor.ConcurrentModificationExceptionis 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
Vecguarantees — the small print under this page: why the pointer is never null but need not point anywhere, whycapacity()can be relied on to the element, and the three things std refuses to promise Vecmethods — one page per method, with a compiled example each: the reference this lesson is the introduction to- Arrays and slices — the type
Vecderefs 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 — whatvec![vec![0; w]; h]allocates, and the flatter thing most grids should be Box— one value on the heap, whereVecis many- Stack and heap — what the pointer points at, and what it costs to follow
- Ownership and moves — why
let v2 = v1;leavesv1unusable iter,iter_mut,into_iter— the three doors onto aVec, 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 bytearrayis the mutable one ↗ — Python'sVec<u8>, and the design question this page never has to ask: with nomuton a Python name, mutability has to be recorded in the type, so Python needs two byte types where Rust needs oneVecand 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