The move that breaks your code
You have a Vec<String> full of user names. You pass it to a function called process_names. Inside that function, you sort the list and print the results. Back in main, you try to print the original list again. The compiler stops you with a red wall of text.
This happens because process_names didn't just look at the list. It took the list. You can't print something you no longer hold. In languages like Python or JavaScript, assigning a list to a variable creates a new reference to the same data, and the original variable stays valid. Rust does something different. Rust moves the ownership. The original variable becomes invalid the moment you hand it off.
Understanding this move is the key to writing efficient Rust. Moving a vector is incredibly cheap. It doesn't copy the data. It just transfers the control. Once you see how the move works, you'll stop fighting the compiler and start using ownership to your advantage.
What a Vec actually is
A Vec<T> is not a contiguous block of data on the stack. It is a small struct that lives on the stack and points to data on the heap. The struct contains three pieces of information:
- A pointer to the heap allocation where the elements live.
- The length, which is the number of elements currently in the vector.
- The capacity, which is the total amount of space allocated on the heap.
When you write let v = vec![1, 2, 3], Rust allocates memory on the heap for the integers. It then creates a Vec struct on the stack with the pointer, length, and capacity. The stack struct is the owner. It is responsible for freeing the heap memory when it goes out of scope.
This structure matters because it explains why moving is fast. The stack struct is tiny. It's just three words of data. The heap allocation can be huge. Moving the vector means moving the tiny struct. The huge data stays exactly where it is.
Moving the handle, not the house
Think of a Vec like a deed to a house. The house is the data on the heap. The deed is the struct on the stack. The deed tells you where the house is and who owns it.
When you assign let v2 = v1, you are handing the deed to someone else. The house doesn't move. The bricks and mortar stay put. Only the deed changes hands. Once you hand over the deed, you no longer own the house. You can't sell the house again because you don't have the deed. Rust enforces this rule at compile time. If you try to use v1 after the assignment, the compiler knows you no longer hold the deed and rejects the code.
This design prevents a common bug called a double-free. If both v1 and v2 owned the same heap allocation, both would try to free the memory when they go out of scope. The second free would corrupt memory and crash the program. Rust avoids this by ensuring there is always exactly one owner. Assignment transfers that ownership.
Minimal example
Here is the simplest case of moving a vector. The code compiles and runs, but notice how v1 disappears after the assignment.
fn main() {
// Create a vector on the heap. v1 owns the data.
let v1 = vec![10, 20, 30];
// Move ownership to v2.
// The pointer, length, and capacity are copied to v2.
// v1 is invalidated immediately.
let v2 = v1;
// v2 can use the data.
println!("v2: {v2}");
// This line would cause a compile error.
// println!("v1: {v1}");
}
The move happens at let v2 = v1. Rust copies the three fields of the Vec struct from v1 to v2. Then the compiler marks v1 as moved. Any attempt to read or write v1 after this point triggers an error. The data on the heap is untouched. v2 now points to the same memory that v1 pointed to, but v1 is no longer allowed to touch it.
What happens in memory
Visualizing the memory helps clarify why this is safe and fast.
Before the move, the stack has v1 with a pointer to heap address 0x1000. The heap at 0x1000 contains [10, 20, 30]. The stack struct also holds length 3 and capacity 3.
When let v2 = v1 executes, the CPU copies the pointer, length, and capacity from the memory location of v1 to the memory location of v2. This is a single instruction on modern hardware. It takes nanoseconds. The heap data is not touched. No allocation occurs. No bytes are copied from the heap.
After the copy, the borrow checker conceptually invalidates v1. It doesn't zero out the memory. It just tracks that v1 is no longer valid. If you try to use v1, the compiler catches it. This tracking happens entirely at compile time. There is zero runtime cost for ownership tracking. The move is as fast as copying three integers.
Realistic scenario: Structs and functions
Moves happen everywhere, not just in variable assignments. Functions take ownership by default. Structs move their fields when the struct moves.
Consider a configuration loader. You read settings into a vector and pass them to a struct.
struct AppConfig {
// The struct owns the vector.
allowed_ips: Vec<String>,
}
fn load_config() -> AppConfig {
// Simulate loading data.
let ips = vec!["192.168.1.1".to_string(), "10.0.0.1".to_string()];
// Move the vector into the struct.
// ips is no longer valid after this.
AppConfig { allowed_ips: ips }
}
fn main() {
let config = load_config();
// config owns the vector now.
println!("First IP: {}", config.allowed_ips[0]);
}
The vector moves from ips into the allowed_ips field of AppConfig. The variable ips is dead inside load_config. The struct takes over ownership. When load_config returns, the struct moves to config in main. The heap data never moves. Only the ownership chain changes.
This pattern is common. Functions often take ownership of data to process it. If a function needs to keep the data, it takes Vec<T> by value. If it only needs to read the data, it takes a reference.
Pitfalls and compiler errors
The compiler protects you from using moved values. You will see these errors often when you are learning.
E0382: use of moved value
This is the most common error. You tried to use a variable after moving it.
let v1 = vec![1, 2];
let v2 = v1;
println!("{v1}"); // Error E0382
The compiler rejects this with E0382. The fix depends on what you need. If you need both variables to own the data, clone the vector. If you only need to read the data in the second place, use a reference.
E0507: cannot move out of borrowed content
This error happens when you try to move a value out of a reference. You have a &Vec<T>, but you try to assign it to a Vec<T>.
fn process(data: &Vec<i32>) {
// Error E0507: cannot move out of borrowed content.
// data is a reference. You can't steal the Vec.
let owned: Vec<i32> = *data;
}
You cannot move ownership from a reference. A reference only borrows the data. It doesn't own it. To get ownership, you must clone the data.
fn process(data: &Vec<i32>) {
// Clone creates a new Vec with new heap allocation.
let owned: Vec<i32> = data.clone();
}
Cloning allocates new memory and copies the elements. This is expensive for large vectors. Use it only when you truly need independent ownership.
Convention aside: Prefer slices for function arguments
When writing functions that read data, avoid taking &Vec<T> or Vec<T>. Take &[T] instead. A slice is a view into the data. It works with vectors, arrays, and other types. It is more flexible and idiomatic.
// Bad: forces caller to provide a Vec.
fn print_items(items: &Vec<String>) { ... }
// Good: accepts any slice of Strings.
fn print_items(items: &[String]) { ... }
Taking Vec<T> by value forces the caller to move or clone. Taking &Vec<T> ties the function to a specific type. Slices decouple the function from the storage type. Trust the borrow checker. It usually has a point.
Decision matrix
Choose the right approach based on who needs ownership and how long the data must live.
Use assignment to transfer ownership when the receiver needs full control and the sender is done with the data. This is the cheapest option. No allocation occurs. The receiver can modify, extend, or drop the vector without affecting the sender.
Use a reference when the receiver only needs to read or modify the vector temporarily, and the sender must retain ownership. References are zero-cost. They allow multiple readers or one writer. The data stays alive as long as the owner exists.
Use clone() when you need two independent vectors that can be modified separately without affecting each other. Cloning allocates new memory and copies the elements. Use this when the lifetimes diverge or when both sides need to mutate the data.
Use Rc<Vec<T>> or Arc<Vec<T>> when multiple parts of the program need shared ownership and the data must outlive individual scopes. Reference counting allows multiple owners. The data is freed when the last owner drops. This adds a small runtime overhead for the counter.
Match the ownership model to the lifetime requirement. Don't clone unless you have to.