Ownership is the idea that makes Rust Rust. It is how the language manages memory without a garbage collector and without asking you to call free. Every value has one owner, the owner decides how long the value lives, and when the owner goes away the value is cleaned up automatically. The compiler tracks all of this at compile time and rejects programs that break the rules.
Almost every confusing compiler error in your first weeks of Rust is an ownership or borrowing error, and almost every Rust interview includes ownership questions. This lesson builds the mental model from the ground up: how the stack and heap work, the three ownership rules, why String behaves differently from a string literal, what a move is, how Copy and Clone differ, how ownership passes into and out of functions, and when values are dropped. By the end you will be able to predict which lines compile, read the "use of moved value" error, and explain how ownership rules out double frees and use-after-free bugs.
Stack and heap
A running program stores data in two main regions of memory. You do not need to know how an operating system implements them, only how they behave.
The stack stores data in the order functions are called. Each function call gets a stack frame, a block holding its local variables. When the function returns, its whole frame is discarded at once. Allocating on the stack is extremely fast: it just moves a pointer. The catch is that every value on the stack must have a size known at compile time.
The heap stores data whose size is decided at run time, or that must outlive the function that created it. To put data on the heap, the program asks the allocator for a block of a given size and gets back a pointer, the memory address of the block. Heap allocation is slower than stack allocation, and the block must be given back to the allocator (freed) exactly once when it is no longer needed.
| Stack | Heap | |
|---|---|---|
| Size of data | Fixed, known at compile time | Can be decided at run time and can grow |
| Allocation | Move the stack pointer; very fast | Ask the allocator; slower |
| Freed when | The function returns | Someone frees it explicitly |
| Access | Directly | Through a pointer |
| Rust examples | i32, f64, bool, char, arrays, tuples of these, the String struct itself | The characters of a String, the elements of a Vec |
Freeing heap memory correctly is the hard part. Free too early and you get a use-after-free; free twice and you get a double free; never free and you get a leak. C makes this your job. Java and Python hand it to a garbage collector. Rust hands it to ownership.
What a String looks like in memory
String is Rust's growable, heap-allocated text type. A String value has two parts:
- A small, fixed-size struct of three machine words on the stack: a pointer to the heap buffer, the length (bytes in use) and the capacity (bytes allocated).
- The bytes of the text, in the heap buffer.
use std::mem::size_of;
fn main() {
let mut name = String::with_capacity(10);
name.push_str("Rust");
println!("len = {}, capacity = {}", name.len(), name.capacity());
println!(
"size of the String value itself: {} bytes",
size_of::<String>()
);
println!("size of a &str: {} bytes", size_of::<&str>());
println!("size of an i32: {} bytes", size_of::<i32>());
}
Output (on a 64-bit machine):
len = 4, capacity = 10
size of the String value itself: 24 bytes
size of a &str: 16 bytes
size of an i32: 4 bytes
String::with_capacity(10) allocated room for 10 bytes, and pushing "Rust" used 4 of them. The String struct itself is 24 bytes: three 8-byte words. A &str (string slice) is 16 bytes: a pointer and a length, with no capacity because a slice cannot grow.
STACK HEAP
name: String
+----------+-------+
| ptr | o---+--------> +---+---+---+---+---+---+---
| len | 4 | | R | u | s | t | | | ...
| capacity | 10 | +---+---+---+---+---+---+---
+----------+-------+ (10 bytes allocated, 4 used)
String versus string literals
A string literal such as "hello" is different. Its bytes are baked into the compiled program, in a read-only section of the executable that exists for the program's entire run. A literal has type &'static str: a reference to text that lives forever ('static). Nothing ever needs to free it, so literals raise no ownership questions.
A String is created at run time, can grow and shrink, and owns a heap buffer that must be freed. That is where ownership matters.
"hello" (literal) | String::from("hello") | |
|---|---|---|
| Type | &'static str | String |
| Bytes live in | The program's read-only data | A heap buffer |
| Can grow or change | No | Yes, if the variable is mut |
| Must be freed | No | Yes, automatically, when its owner goes away |
| Size on stack | 16 bytes (pointer, length) | 24 bytes (pointer, length, capacity) |
The three ownership rules
Everything in this lesson follows from three rules:
- Each value in Rust has an owner. The owner is usually a variable, but it can also be a field of a struct, an element of a collection, or a function parameter.
- There can only be one owner at a time.
- When the owner goes out of scope, the value is dropped. Dropping a value runs its cleanup code, which for a
Stringfrees the heap buffer.
A scope is the region of code where a name is valid, usually from the let that creates it to the closing brace of the enclosing block.
{ // s is not valid yet
let s = String::from("hello"); // s is valid from here
// use s
} // scope ends: s is dropped, buffer freed
At the closing brace, Rust automatically calls the cleanup code for s. This is the same idea as RAII in C++ ("resource acquisition is initialisation"): tie a resource's lifetime to an object's scope, and release the resource when the object is destroyed. Rust applies it everywhere and, crucially, makes the compiler enforce rule 2, which C++ does not.
Moves
What happens when you assign one String variable to another?
let s1 = String::from("hello");
let s2 = s1;
Rust copies the three-word struct (pointer, length, capacity) from s1 into s2. It does not copy the heap bytes. Now two structs point at the same buffer:
s1 (no longer valid) HEAP
+----------+-----+
| ptr | o--+--------+
| len | 5 | |
| capacity | 5 | +----> +---+---+---+---+---+
+----------+-----+ | | h | e | l | l | o |
s2 (owner) | +---+---+---+---+---+
+----------+-----+ |
| ptr | o--+--------+
| len | 5 |
| capacity | 5 |
+----------+-----+
If both s1 and s2 were considered owners, both would free the buffer when they went out of scope: a double free. Rust avoids this with rule 2. After let s2 = s1;, ownership has moved to s2, and s1 is no longer valid. Only s2 will free the buffer.
Using s1 after the move is a compile error. This does not compile:
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{s1}, world");
println!("{s2}");
}
error[E0382]: borrow of moved value: `s1`
--> move_err.rs:4:16
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 | let s2 = s1;
| -- value moved here
4 | println!("{s1}, world");
| ^^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
3 | let s2 = s1.clone();
| ++++++++
Reading the compiler error: E0382
This is the error you will see most often while learning ownership. The labels tell a three-part story, top to bottom:
- "move occurs because
s1has typeString, which does not implement theCopytrait": the reason a move happened at all. Types that areCopyare duplicated instead of moved (see below). - "value moved here": the exact place ownership left
s1. - "value borrowed here after move": where you then tried to use it.
println!borrows its arguments, which is why the message says "borrowed" rather than "used".
The help: offers s1.clone(), which makes a full copy so both variables own separate data. That is sometimes right, but not always: the next lesson shows that borrowing is usually the better fix.
A move in Rust is cheap: it copies a few bytes of stack data and nothing on the heap. It is a shallow copy plus invalidation of the source. Compare with other languages:
| Language | b = a for a string-like object | After that |
|---|---|---|
| Python, Java, JavaScript | Both names refer to the same object | Both usable; GC frees it when no references remain |
C++ (std::string) | Deep copy by default; std::move(a) moves | After std::move, a is still usable but in an unspecified state |
Rust (String) | Move: shallow copy, source invalidated | Using a is a compile error |
The C++ comparison is worth remembering for interviews: a moved-from C++ object still exists and can be accidentally used, while a moved-from Rust variable cannot be used at all, and Rust does not run cleanup code for it.
Moving a value that contains a String
The move rule applies to any type that owns heap data, including tuples and structs that contain a String. This does not compile:
fn main() {
let pair = (String::from("id"), 7);
let other = pair;
println!("{:?} {:?}", pair, other);
}
error[E0382]: borrow of moved value: `pair`
--> tuple_move_err.rs:4:27
|
2 | let pair = (String::from("id"), 7);
| ---- move occurs because `pair` has type `(String, i32)`, which does not implement the `Copy` trait
3 | let other = pair;
| ---- value moved here
4 | println!("{:?} {:?}", pair, other);
| ^^^^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
3 | let other = pair.clone();
| ++++++++
The tuple (String, i32) is not Copy because one of its parts is not. Assigning it moves the whole tuple.
Reassigning a variable drops the old value
A mut variable can be given a new value. When that happens, the old value is dropped first, because nothing owns it any more. This program uses a small struct with a custom Drop implementation (lesson 6 covers structs, and lesson 15 covers Drop) that prints a message when a value is dropped:
struct Noisy(&'static str);
impl Drop for Noisy {
fn drop(&mut self) {
println!("dropping {}", self.0);
}
}
fn main() {
let mut slot = Noisy("old");
println!("holding {}", slot.0);
slot = Noisy("new");
println!("holding {}", slot.0);
}
Output:
holding old
dropping old
holding new
dropping new
The assignment slot = Noisy("new") dropped "old" immediately. "new" was dropped at the end of main. You can also assign a fresh value to a variable that was moved out of; the variable becomes valid again.
Clone: an explicit deep copy
When you really need two independent copies of heap data, call clone():
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {s1}, s2 = {s2}");
let a = 42;
let b = a;
println!("a = {a}, b = {b}");
let point = (3, 4);
let copy = point;
println!("point = {point:?}, copy = {copy:?}");
}
Output:
s1 = hello, s2 = hello
a = 42, b = 42
point = (3, 4), copy = (3, 4)
s1.clone() allocates a new heap buffer and copies the bytes into it. Now s1 and s2 each own their own buffer, and each frees its own when dropped.
s1 HEAP
+-----+-----+-----+ +---+---+---+---+---+
| ptr | len | cap |-----> | h | e | l | l | o |
+-----+-----+-----+ +---+---+---+---+---+
s2
+-----+-----+-----+ +---+---+---+---+---+
| ptr | len | cap |-----> | h | e | l | l | o | (separate copy)
+-----+-----+-----+ +---+---+---+---+---+
Cloning can be expensive (a Vec of a million strings clones a million strings), which is exactly why Rust never does it implicitly. When you see .clone() in Rust code, you know a potentially costly copy is happening. In C++, the same copy can hide inside an innocent-looking b = a;.
Copy types
The second half of clone.rs shows integers and tuples of integers behaving differently: after let b = a;, both a and b are usable. That is because they are Copy types.
A type is Copy if duplicating it is just copying its bits, with no heap data and no cleanup to worry about. Assigning or passing a Copy value copies it, and the original stays valid. Copy is a trait, a label that a type opts into (traits are covered in lesson 12).
| Copy | Not Copy |
|---|---|
All integer types, f32, f64 | String |
bool, char | Vec<T>, HashMap<K, V>, Box<T> |
Shared references &T | Mutable references &mut T |
Tuples and arrays whose elements are all Copy, such as (i32, f64) or [u8; 4] | Tuples, arrays or structs containing any non-Copy part |
Structs and enums that derive Copy (all fields must be Copy) | Any type that implements Drop |
A type cannot be both Copy and have custom cleanup code. If a type needs cleanup, copying its bits would duplicate the resource and lead straight back to double frees.
Clone versus Copy
Copy | Clone | |
|---|---|---|
| How it happens | Implicitly, on every assignment or pass | Explicitly, by calling .clone() |
| What it does | Bitwise copy of the value | Whatever the type's clone method does, often a deep copy |
| Cost | Always cheap | Can be expensive |
| Requirement | Every part must be Copy; no Drop | Any type can implement it |
| Relationship | Every Copy type is also Clone | Not every Clone type is Copy |
You can ask the compiler to implement both for your own type with #[derive(Clone, Copy)], but only if every field is Copy. This does not compile:
#[derive(Clone, Copy)]
struct User {
name: String,
age: u32,
}
fn main() {
let u = User {
name: String::from("Ira"),
age: 30,
};
println!("{} {}", u.name, u.age);
}
error[E0204]: the trait `Copy` cannot be implemented for this type
--> copy_err.rs:2:8
|
1 | #[derive(Clone, Copy)]
| ---- in this derive macro expansion
2 | struct User {
| ^^^^
3 | name: String,
| ------------ this field does not implement `Copy`
Remove Copy from the derive (keep Clone) and it compiles; users of the type then call .clone() explicitly.
Interview tip
A crisp answer to "Copy vs Clone?": Copy is an implicit, bitwise, always-cheap duplicate for simple stack-only types, and it changes assignment from a move into a copy. Clone is an explicit, possibly expensive duplicate that any type can implement. Every Copy type is Clone, and a type with a destructor (Drop) can never be Copy.
Ownership and functions
Passing a value to a function works exactly like assigning it to a variable: a non-Copy value is moved into the parameter, and a Copy value is copied.
fn takes_ownership(text: String) {
println!("got: {text}");
}
fn makes_copy(n: i32) {
println!("got: {n}");
}
fn main() {
let s = String::from("report.pdf");
takes_ownership(s);
let n = 5;
makes_copy(n);
println!("n is still usable: {n}");
}
Output:
got: report.pdf
got: 5
n is still usable: 5
Trace the ownership:
main takes_ownership
s = String("report.pdf")
takes_ownership(s) --- move --> text owns the String
(s is invalid now) prints it
} text dropped: buffer freed
n = 5
makes_copy(n) ------- copy ---> n (its own copy)
n still usable
When takes_ownership returns, its parameter text goes out of scope and the String is dropped. If main then tries to use s, it gets E0382. This does not compile:
fn takes_ownership(text: String) {
println!("got: {text}");
}
fn main() {
let s = String::from("report.pdf");
takes_ownership(s);
println!("{s}");
}
error[E0382]: borrow of moved value: `s`
--> fn_move_err.rs:8:16
|
6 | let s = String::from("report.pdf");
| - move occurs because `s` has type `String`, which does not implement the `Copy` trait
7 | takes_ownership(s);
| - value moved here
8 | println!("{s}");
| ^ value borrowed here after move
|
note: consider changing this parameter type in function `takes_ownership` to borrow instead if owning the value isn't necessary
--> fn_move_err.rs:1:26
|
1 | fn takes_ownership(text: String) {
| --------------- ^^^^^^ this parameter takes ownership of the value
| |
| in this function
help: consider cloning the value if the performance cost is acceptable
|
7 | takes_ownership(s.clone());
| ++++++++
The compiler's notes are excellent here. It points at the parameter that "takes ownership of the value" and suggests two options: change the parameter to borrow (lesson 5), or clone at the call site.
Returning ownership
A function can also give ownership back by returning a value. Returning moves the value out to the caller:
fn make_greeting(name: &str) -> String {
let mut greeting = String::from("Hello, ");
greeting.push_str(name);
greeting
}
fn add_suffix(mut text: String) -> String {
text.push('!');
text
}
fn main() {
let g = make_greeting("Anil");
let g = add_suffix(g);
println!("{g}");
}
Output:
Hello, Anil!
make_greetingcreates aStringand returns it. Ownership moves toginmain; nothing is freed whenmake_greetingends, because its localgreetingwas moved out.add_suffixtakes ownership with amutparameter, modifies the string and hands it back. Shadowinggkeeps the name.
Passing ownership in and getting it back works, but doing it for every function that only needs to read a value would be tedious. Imagine returning a tuple (String, usize) just to report a length. That is the problem references solve in the next lesson.
Moves are not runtime work
"Moving" is a compile-time idea about who is responsible for a value. At run time, a move copies at most a few bytes, and the optimiser often removes even that. Rust does not track ownership at run time, and there is no reference count or ownership flag in the compiled program for ordinary values.
Drop and scope in detail
When a value is dropped, Rust calls its destructor: the cleanup code for its type. For String and Vec, the destructor frees the heap buffer. For a file handle, it closes the file. For a lock guard, it releases the lock. You can write your own by implementing the Drop trait. The rules for when this happens:
- Local variables are dropped at the end of their scope, in reverse order of declaration: the last variable created is dropped first.
- A value that was moved away is not dropped where it used to be; the new owner is responsible.
- You can drop a value early with
drop(value), a standard function that simply takes ownership and lets the value go out of scope.
struct Noisy {
name: &'static str,
}
impl Drop for Noisy {
fn drop(&mut self) {
println!("dropping {}", self.name);
}
}
fn main() {
let _a = Noisy { name: "a" };
{
let _b = Noisy { name: "b" };
println!("end of inner scope");
}
let c = Noisy { name: "c" };
let _d = Noisy { name: "d" };
drop(c);
println!("end of main");
}
Output:
end of inner scope
dropping b
dropping c
end of main
dropping d
dropping a
Walk through it:
_ais created inmain's scope.- The inner block creates
_b, prints, and ends;_bis dropped there. cand_dare created.drop(c)movescinto thedropfunction, which drops it immediately.mainprints "end of main" and then reaches its closing brace. The remaining values are dropped in reverse order of declaration:_dfirst, then_a.cis not dropped again, because it was moved away.
The variable names start with _ so the compiler does not warn that they are unused. Be careful with a bare _, though: let _ = Noisy { ... }; does not bind the value at all, so it is dropped immediately on that line.
Common mistake: calling the destructor yourself
You cannot call value.drop() directly; the compiler rejects it (error E0040) because Rust would then drop the value a second time at the end of the scope. Use the free function drop(value), which takes ownership so the automatic drop does not happen.
Why this prevents memory bugs
Now the big picture. Each classic memory bug maps to a rule the compiler enforces:
| Bug | How it happens in C/C++ | Why it cannot happen in safe Rust |
|---|---|---|
| Double free | Two pointers to one block, both freed | Only one owner at a time; a move invalidates the source, so only one drop runs |
| Use after free | A pointer is used after the block is freed | After a move, the old variable cannot be used (E0382); references cannot outlive the owner (lesson 5) |
| Memory leak by forgetting free | malloc without free | The owner's drop runs automatically at scope end |
| Freeing too early | free while someone still uses the data | You cannot free a value you have lent out; borrows must end first (lesson 5) |
| Freeing a literal or stack value | free on the wrong pointer | You never call free; Rust picks the right cleanup for each type |
Two clarifications keep you accurate in interviews. First, Rust prevents leaks in normal code but does not guarantee leak freedom: reference-counted cycles (lesson 15) and the deliberate std::mem::forget can leak memory, and leaking is considered memory safe. Second, all of this is checked at compile time; the compiled program contains the same free calls a careful C programmer would write, in the same places, with no garbage collector running.
More moves you will meet
Moving inside a loop
A value can only be moved once. Moving it inside a loop body would move it again on the second iteration. This does not compile:
fn consume(text: String) {
println!("{text}");
}
fn main() {
let message = String::from("hi");
for _ in 0..2 {
consume(message);
}
}
error[E0382]: use of moved value: `message`
--> loop_move_err.rs:8:17
|
6 | let message = String::from("hi");
| ------- move occurs because `message` has type `String`, which does not implement the `Copy` trait
7 | for _ in 0..2 {
| ------------- inside of this loop
8 | consume(message);
| ^^^^^^^ value moved here, in previous iteration of loop
|
note: consider changing this parameter type in function `consume` to borrow instead if owning the value isn't necessary
--> loop_move_err.rs:1:18
|
1 | fn consume(text: String) {
| ------- ^^^^^^ this parameter takes ownership of the value
| |
| in this function
help: consider cloning the value if the performance cost is acceptable
|
8 | consume(message.clone());
| ++++++++
Note the phrase "value moved here, in previous iteration of loop". The fix depends on intent: clone the value for each call, create a new value inside the loop, or change consume to borrow its argument.
Moving out of a vector by index
Indexing a vector gives you access to an element the vector still owns. You cannot move the element out with plain indexing, because that would leave a hole in the vector. This does not compile:
fn main() {
let names = vec![String::from("Asha"), String::from("Ben")];
let first = names[0];
println!("{first}");
}
error[E0507]: cannot move out of index of `Vec<String>`
--> vec_move_err.rs:3:17
|
3 | let first = names[0];
| ^^^^^^^^ move occurs because value has type `String`, which does not implement the `Copy` trait
|
help: consider borrowing here
|
3 | let first = &names[0];
| +
help: consider cloning the value if the performance cost is acceptable
|
3 | let first = names[0].clone();
| ++++++++
The compiler suggests borrowing (&names[0]) or cloning. If you really want to take ownership of elements, use a method that keeps the vector consistent:
fn main() {
let mut names = vec![
String::from("Asha"),
String::from("Ben"),
String::from("Chen"),
];
let first_copy = names[0].clone();
let taken = std::mem::take(&mut names[1]);
let last = names.pop().unwrap();
let removed = names.remove(0);
println!("{first_copy} {taken} {last} {removed}");
println!("left: {names:?}");
for name in [String::from("x"), String::from("y")] {
println!("owned in loop: {name}");
}
}
Output:
Asha Ben Chen Asha
left: [""]
owned in loop: x
owned in loop: y
Step by step, starting from ["Asha", "Ben", "Chen"]:
names[0].clone()copies"Asha"; the vector is unchanged.std::mem::take(&mut names[1])moves"Ben"out and leaves an emptyStringin its place:["Asha", "", "Chen"].names.pop()removes and returns the last element,"Chen". It returns anOption, because the vector might be empty;unwrap()takes the value out (lesson 9 discusses when that is acceptable).names.remove(0)removes"Asha"and shifts the rest left, leaving[""].- A
forloop over a vector by value (not&vec) consumes the vector and moves each element into the loop variable in turn.
Idioms and pitfalls
Idiom: take ownership only when you need it
A function should take a String by value only when it stores it, returns it, or otherwise needs to own it. If it only reads the value, it should borrow it (&str), which lesson 5 covers. Getting this right is most of what "designing with ownership" means.
Pitfall: cloning to silence the compiler
Adding .clone() everywhere makes errors disappear, but it hides design problems and costs memory and time. Before cloning, ask whether the receiver could borrow instead, or whether the value could be moved because the original is no longer needed. Clone when you genuinely need two independent copies.
Pitfall: thinking a move copies the heap data
let b = a; for a String does not copy the text. It copies the 24-byte handle and transfers responsibility. Moves of large heap structures are therefore cheap, and you should not avoid them for performance reasons.
Exercises
Exercise 1: fix the move
This fails with E0382 because city is used after being moved:
let city = String::from("Hyderabad");
let moved = city;
println!("{moved} {city}");
Fix it so both names can be printed, keeping the let moved = city; line.
Solution
Make an independent copy before the move, and print the copy instead of the moved-from variable.
fn main() {
let city = String::from("Hyderabad");
let backup = city.clone();
let moved = city;
println!("{moved} {backup}");
}
Hyderabad Hyderabad
Exercise 2: copy or move?
Predict which of these variables are still usable after the assignments, then run the program.
fn main() {
let a = 10;
let b = a;
let t = (1.5, true);
let u = t;
let s = String::from("x");
let v = s.clone();
let arr = [1, 2, 3];
let arr2 = arr;
println!("{a} {b} {t:?} {u:?} {s} {v} {arr:?} {arr2:?}");
}
Solution
All of them. a is an i32, t is a tuple of f64 and bool, and arr is an array of i32, so all three are Copy and assignment copies them. s would have been moved, but v is a clone, so s was never moved.
10 10 (1.5, true) (1.5, true) x x [1, 2, 3] [1, 2, 3]
If you change let v = s.clone(); to let v = s;, the program fails with E0382 on {s}.
Exercise 3: give it back
Write fn measure(text: String) -> (String, usize) that returns the string together with its length in bytes, and use it from main without cloning.
Solution
The function takes ownership, computes the length, and moves the string back out inside the tuple.
fn measure(text: String) -> (String, usize) {
let length = text.len();
(text, length)
}
fn main() {
let s = String::from("ownership");
let (s, len) = measure(s);
println!("{s} has {len} bytes");
}
ownership has 9 bytes
This works, but it is clumsy. In lesson 5 the same function becomes fn measure(text: &str) -> usize.
Exercise 4: fix the loop
Fix the "moved in previous iteration of loop" program from earlier so that it calls consume twice and can still print message afterwards, without changing consume.
Solution
Give consume its own copy on each iteration.
fn consume(text: String) {
println!("{text}");
}
fn main() {
let message = String::from("hi");
for _ in 0..2 {
consume(message.clone());
}
println!("still mine: {message}");
}
hi
hi
still mine: hi
Once you know borrowing, the better fix is to change consume to take &str.
Exercise 5: predict the drop order
Predict the exact output of this program before running it.
struct Noisy(&'static str);
impl Drop for Noisy {
fn drop(&mut self) {
println!("drop {}", self.0);
}
}
fn pass_through(n: Noisy) -> Noisy {
println!("inside pass_through");
n
}
fn swallow(_n: Noisy) {
println!("inside swallow");
}
fn main() {
let x = Noisy("x");
let y = Noisy("y");
let x = pass_through(x);
swallow(y);
println!("end of main, still holding {}", x.0);
}
Solution
inside pass_through
inside swallow
drop y
end of main, still holding x
drop x
x moves into pass_through and is moved back out, so it is not dropped there. y moves into swallow, which never returns it, so it is dropped when swallow ends. The final x (the shadowing variable holding the returned value) is dropped at the end of main. The original x and y variables were moved from, so nothing is dropped for them.
Interview questions
Q1. What are Rust's ownership rules?
Each value has a single owner; there can be only one owner at a time; and when the owner goes out of scope the value is dropped, which runs its destructor and frees any resources such as heap memory. Ownership can be transferred (moved) by assignment, by passing to a function or by returning from one. The compiler enforces these rules at compile time.
Q2. What is a move, and what does it cost?
A move transfers ownership of a value from one variable (or parameter, or field) to another. At run time it is a shallow copy of the value's stack representation, for a String the 24-byte pointer, length and capacity, and no heap data is copied. The compiler then treats the source as invalid, so using it is an error and no destructor runs for it.
Q3. What is the difference between Copy and Clone?
Copy types are duplicated implicitly with a bitwise copy whenever they are assigned or passed, and the original remains usable; it applies only to simple types with no heap ownership and no Drop. Clone is an explicit .clone() call that may perform an expensive deep copy and can be implemented by any type. All Copy types are also Clone.
Q4. Why can a type that implements Drop not be Copy?
Drop means the type has cleanup to perform, usually releasing a resource it owns. If such a type were copied bit for bit, two values would think they own the same resource and both would release it, which is a double free. Rust therefore forbids combining Copy and Drop.
Q5. How does ownership prevent double frees and use-after-free?
Only one owner exists at a time, and only the owner's drop frees the memory, so the memory is freed exactly once. A moved-from variable cannot be used, and references cannot outlive the value they point to, so nothing can access memory after it is freed. All of this is proven at compile time.
Q6. When is a value dropped?
When its owner goes out of scope, local variables are dropped in reverse order of declaration. A value is also dropped when a variable holding it is assigned a new value, when it is passed to drop, or when a function that took ownership of it ends without moving it elsewhere. A moved-from variable is not dropped.
Q7. How is Rust's move different from C++'s std::move?
In C++, std::move only casts to an rvalue reference so that a move constructor can run; the source object remains a valid object in an unspecified state, can still be used by mistake, and its destructor still runs. In Rust, a move is the default for non-Copy types, the source becomes unusable as a compile-time rule, and no destructor runs for it.
Q8. What is the difference between a string literal and a String in terms of ownership?
A string literal is a &'static str: a reference to bytes embedded in the program binary that live for the whole run, so there is nothing to own or free. A String owns a heap buffer that it frees when dropped, and it can grow. Converting a literal into a String with String::from or to_string allocates and copies the bytes.
Q9. Does Rust guarantee there are no memory leaks?
No. Ordinary code frees memory automatically, so leaks are rare, but leaking is considered memory safe. std::mem::forget, Box::leak and reference-count cycles with Rc can all leak memory in safe code. Rust's guarantee is about never accessing invalid memory, not about always freeing it.
Q10. Why can you not move an element out of a Vec with indexing?
Moving the element out would leave the vector with an invalid slot that it would later try to drop again. Indexing therefore only gives access to an element still owned by the vector. To take ownership, use methods that keep the vector consistent, such as remove, pop, swap_remove, std::mem::take or std::mem::replace, or clone the element.
Key takeaways
- The stack holds fixed-size data freed automatically when a function returns; the heap holds run-time-sized data that must be freed exactly once.
- A
Stringis a 24-byte handle (pointer, length, capacity) on the stack plus a heap buffer; a literal is a&'static strbaked into the binary. - Three rules: every value has an owner, there is one owner at a time, and the value is dropped when the owner goes out of scope.
- Assigning or passing a non-Copy value moves it: the handle is copied, the heap is not, and the source becomes unusable (E0382).
Copytypes are duplicated implicitly;Cloneis an explicit, possibly expensive copy. Types withDropcannot beCopy.- Functions take ownership of non-Copy arguments and can give ownership back by returning values.
- Values drop at scope end in reverse declaration order, on reassignment, or early with
drop(value). - Together these rules rule out double frees and use-after-free at compile time with no runtime cost.
Next lesson
Continue with Borrowing and references in Rust.

