Vec: push, index and grow
In Rust Vec<T> is the growable sequence: a pointer, a length and a capacity into a heap buffer, with the same ownership rules as String. It is the container to reach for by default, and most of the standard library is written against it or against a slice of it.
This lesson covers the operations a first program needs: build, push, read, grow, insert, remove and walk. The sections on indexing and capacity are the ones that matter later, because they explain the difference between a length and a capacity and why a bounds check exists.
Building a Vec
Vec::new() starts empty and needs a type annotation unless the first push supplies one. push appends a value and grows the buffer when it is full. The debug format prints the elements in square brackets, and len counts them.
The program builds a vector of three integers and prints it. The annotation is on the binding, and the values are the ones the three pushes give.
fn main() {
let mut values: Vec<i32> = Vec::new();
values.push(3);
values.push(5);
values.push(7);
println!("{values:?} len {}", values.len());
}
rustc --edition 2024 new.rs
./new
[3, 5, 7] len 3
The macro vec! builds the same value in one expression, which the later programs use. The lesson on ownership explains why the vector owns its elements and moves when it is assigned.
Index and get
values[1] returns a reference to the element and panics when the index is out of bounds. values.get(1) returns an Option and never panics, which is the right choice when the index comes from outside the program.
The program reads one element both ways and asks for an index that does not exist. The debug print shows Some(20) and None, which is the whole difference.
fn main() {
let values = vec![10, 20, 30];
println!("{}", values[1]);
println!("{:?} {:?}", values.get(1), values.get(5));
}
rustc --edition 2024 index.rs
./index
20
Some(20) None
The square brackets are the Index trait from the lesson on operator traits, and the panic is the bounds check. The lesson on Option shows how to handle the None case.
Length against capacity
The length is how many values the vector holds, and the capacity is how many it could hold without allocating again. A push past the capacity allocates a larger buffer and copies the elements, which is why a push is usually cheap and occasionally expensive.
The program prints both numbers before and after five pushes, then reserves room and prints the capacity. The exact growth is an implementation detail, and the numbers below are what this build did.
fn main() {
let mut values = Vec::new();
println!("{} {}", values.len(), values.capacity());
values.push(1);
values.push(2);
values.push(3);
values.push(4);
values.push(5);
println!("{} {}", values.len(), values.capacity());
values.reserve(10);
println!("{}", values.capacity());
}
rustc --edition 2024 capacity.rs
./capacity
0 0
5 8
16
reserve asks for room for at least the given number of additional elements, so the capacity jumps to sixteen. Reserving before a loop that pushes a known number of values turns many small allocations into one, and it is the first optimisation worth trying when a build of a large vector is slow. The capacity never shrinks on its own: shrink_to_fit asks for that when the vector is finished growing. The lesson on arrays shows the fixed-size alternative with no capacity at all.
Insert and remove
insert and remove work at an index and shift the elements after it, so both are linear in the length. remove returns the value it took out, which is how a queue built on a vector returns its item.
The program removes the second element, inserts a new one at the front, and prints the removed value and the vector. The order of the two operations is visible in the output.
fn main() {
let mut values = vec![1, 2, 3, 4];
let removed = values.remove(1);
values.insert(0, 9);
println!("{removed} {values:?}");
}
rustc --edition 2024 remove.rs
./remove
2 [9, 1, 3, 4]
Both operations take a mutable borrow, so the vector may not be read while they run. The element type must not matter to the shift either: a vector of large structs moves those structs byte for byte, which is why a long sequence of inserts at the front is quadratic. A vector used as a queue with remove(0) shifts every element on each call. The lesson on VecDeque shows the container built for that job.
Walking and changing
for value in &values borrows each element, and for value in &mut values borrows each one mutably. The loop body writes through the reference with *value, which is the same dereference the lesson on references introduced.
The program multiplies every element by ten and then prints the vector twice, once per loop. The second loop reads the values the first one wrote.
fn main() {
let mut values = vec![1, 2, 3];
for value in &mut values {
*value *= 10;
}
for value in &values {
println!("{value}");
}
}
rustc --edition 2024 iterate.rs
./iterate
10
20
30
The vector may not be resized inside either loop, because the loop holds a borrow of the whole sequence. The lesson on iterator adaptors shows the chain form of the same walk.
Exercises
- Build a vector of five names, remove the middle one and print the rest. Which index does the middle one have?
- Push a hundred numbers into an empty vector and print the capacity before and after. How many allocations did the growth take?
- Write a function that takes &mut Vec<i32> and adds one to every element. Which loop form does it need?
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What is a Vec<T>?
Answer
A growable buffer on the heap with a length and a capacity
The vector owns a heap buffer and tracks how many values it holds and how much room it has.
-
What does
values.get(5)return for a three-element vector?Answer
None
get returns an Option, so an index out of bounds is None instead of a panic.
-
What does the capacity of a Vec describe?
Answer
How many values it could hold without allocating again
Capacity is the room in the buffer; length is the number of values in it.
-
Which method adds a value to the end of a Vec?
Answer
push
push appends one value and returns nothing; insert takes an index.
-
What does this program print?
fn main() { let mut values: Vec<i32> = Vec::new(); values.push(3); values.push(5); values.push(7); println!("{values:?} len {}", values.len()); }Answer
[3, 5, 7] len 3
The three pushes give a length of three, and the debug form prints the elements.
-
What does this program print?
fn main() { let values = vec![10, 20, 30]; println!("{}", values[1]); println!("{:?} {:?}", values.get(1), values.get(5)); }Answer
20 / Some(20) None
Index one is the second value, and get returns Some for it and None past the end.
-
What does this program print?
fn main() { let mut values = vec![1, 2, 3, 4]; let removed = values.remove(1); values.insert(0, 9); println!("{removed} {values:?}"); }Answer
2 [9, 1, 3, 4]
remove takes the second element, and insert puts nine at the front of what is left.
-
What does this program print?
fn main() { let mut values = vec![1, 2, 3]; for value in &mut values { *value *= 10; } for value in &values { println!("{value}"); } }Answer
10 / 20 / 30
The first loop writes through mutable references, and the second reads the new values.
-
What is the first line of the compiler output for this program?
fn main() { let mut values = vec![1, 2, 3]; values.push("four"); println!("{values:?}"); }Answer
error[E0308]: mismatched types
The vector holds integers and the push is a string slice, and the note points at the method definition.
-
What does this program print?
fn main() { let mut values = Vec::new(); values.push(1); println!("{} {}", values.len(), values.capacity() >= values.len()); }Answer
1 true
One push gives a length of one, and the capacity is always at least the length.
The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev