🧠 Why Care About These Concepts?
- Reliability These concepts allow the Rust compiler to prevent common resource errors such as:
- Memory leaks
- Dangling pointers
- Double frees
- Accessing uninitialized memory
- Convenience The Rust compiler can automatically free resources using these concepts — no need for manual
free
or a garbage collector. - Performance Resource management without a garbage collector enables:
- Faster performance
- Suitability for real-time systems
🛠️ How to Use These Features?
- These features are enforced by the compiler through compile-time checks.
- You don't usually do something manually — instead, you follow Rust's rules.
- Understanding these concepts is essential to fix compiler error messages during development.
🧾 Basic Memory Management Terminology
- A variable is a name for a memory location holding a value of some type.
- Memory can be allocated in three regions:
- Stack: Automatically allocated when a function is called, and freed when the function returns.
- Heap: Requires explicit allocation and deallocation (handled by Rust’s ownership model).
- Static memory: Lives for the entire duration of the program.
🐞 Common Memory Errors
- Dangling Pointer: A pointer to memory that has been freed or was never initialized.
- Memory Leak: Memory allocated on the heap is never freed — e.g., forgetting to release memory.
- Uninitialized Memory: Using memory before it has been properly allocated or assigned a value.
- Double Free: Attempting to free the same memory more than once — either on the same variable or on a copy.
⚠️ What Can Go Wrong on the Stack?
- Since memory is automatically allocated and freed, there are no memory leaks, uninitialized memory, or double free problems.
- The function might return a pointer to a value on the stack, leading to a dangling pointer.
- Rust prevents this by simply checking that no such references are returned — see
stack-dangling-pointer.rs
.
❌ Example: Returning a reference to a local variable
fn create_ref() -> &i32 {
let number = 10;
&number // ❌ This will cause a compile error
}
Compiler Error:
error[E0515]: cannot return reference to local variable `number`
- Don’t return references to local function variables — copy or move the value out of the function.
✅ Correct version: Move the value out
fn create_value() -> i32 {
let number = 10;
number // ✅ Move the value instead of returning a reference
}
💾 What Can Go Wrong on the Heap?
- A reference might be used after the memory was reallocated or freed, leading to a dangling pointer.
- The borrow checker prevents the reallocation by not allowing a mutable and other reference at the same time. An immutable reference is needed to push on a vector and possibly reallocate it — see
heap-reallocation-dangling-pointer.rs
.
❌ Example: Reallocation with active immutable borrow
fn main() {
let mut vec = vec![1, 2, 3];
let first = &vec[0]; // Immutable borrow
vec.push(4); // ❌ May cause reallocation
println!("{}", first); // Use after potential reallocation
}
Compiler Error:
error[E0502]: cannot borrow `vec` as mutable because it is also borrowed as immutable
- Don’t do it, and if you really need a mutable reference paired with other references, use the
std::cell
module.
✅ Correct version: Borrow after mutation
fn main() {
let mut vec = vec![1, 2, 3];
vec.push(4); // ✅ Mutate first
let first = &vec[0]; // Borrow afterwards
println!("{}", first);
}
- Rust prevents the use after free case by making sure no reference is used after its lifetime has ended, i.e. the value was dropped — see
heap-dropped-dangling-pointer.rs
.
❌ Example: Reference lives longer than the value
fn main() {
let r;
{
let vec = vec![1, 2, 3];
r = &vec[0]; // ❌ `vec` goes out of scope here
}
println!("{}", r); // Use after drop
}
Compiler Error:
error[E0597]: `vec` does not live long enough
- Don’t do it, or if you really run into this problem, you might need shared ownership with the
std::rc
module.
- The borrow checker also does not allow a value to be moved to another variable that could reallocate or free the memory while there are references — see
heap-move-dangling-pointer.rs
.
❌ Example: Move after borrow
fn main() {
let vec = vec![1, 2, 3];
let r = &vec;
let moved = vec; // ❌ Moving vec while r still exists
println!("{:?}", r);
}
Compiler Error:
error[E0505]: cannot move out of `vec` because it is borrowed
- Don’t move a value to another variable and then use a reference to it you created before.
✅ Correct version: Use after move or avoid borrowing before move
fn main() {
let vec = vec![1, 2, 3];
let moved = vec; // ✅ Move without borrowing first
println!("{:?}", moved);
}
🔑 What Is Ownership?
- Rust automatically frees memory, so there are no memory leaks or double free calls.
- Rust does this without a garbage collector.
🧹 How Does Rust Manage Memory?
- The concept is simple: Rust calls a destructor (
drop
) whenever the lifetime of a value ends, i.e., when the value goes out of scope ({}
block ends).
fn main() {
{
let _s = String::from("hello");
// `_s` is dropped here automatically
}
// memory is freed
}
❌ The Problem with Shallow Copies
- Some values, like a
Vec
, contain heap-allocated data. - A shallow copy (only copying pointer and metadata) would cause a double free error if both copies tried to free the same memory.
✅ Rust prevents this with Move Semantics
- When you assign one variable to another, Rust moves the value instead of copying it (unless it implements
Copy
).
🔁 Move Example (move-semantics.rs
)
fn main() {
let vec = vec![1, 2, 3];
let moved = vec; // vec is moved
// println!("{:?}", vec); // ❌ error: use of moved value
}
🧠 Compiler Error:
error[E0382]: borrow of moved value: `vec`
- Don’t use a value after it was moved.
- If you really need a deep copy, use
.clone()
:
fn main() {
let vec = vec![1, 2, 3];
let cloned = vec.clone(); // deep copy
println!("{:?}", vec); // ✅ ok to use
}
⚠️ .clone() is often unnecessary and can lead to performance issues if overused.
📦 Copy vs Move
- Primitive types (e.g.,
i32
,bool
,char
) are copied by default. - No heap data → no double free risk.
fn main() {
let a = 42;
let b = a; // a is copied, not moved
println!("a = {}, b = {}", a, b); // ✅ both are usable
}
- Types with heap data (e.g.,
Vec
,String
) are moved by default. - You can make your own types Copy-able by implementing the
Copy
trait:
Example: Copy
trait (copy-semantics.rs
)
#[derive(Copy, Clone)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = p1; // p1 is copied, not moved
println!("p1: {}, {}", p1.x, p1.y); // ✅ still valid
}
- If a type is not
Copy
, Rust will give an error like:
move occurs because `config` has type `Config`, which does not implement the `Copy` trait
✅ Don’t implement Copy if you can live with using references.
✅ You can also just implement
Clone
and call.clone()
explicitly — seeclone-semantics.rs
.
📌 Summary and Miscellaneous Info
- Ownership, borrowing, and lifetimes enable the Rust compiler to:
- Detect and prevent memory errors
- Handle memory automatically and safely
- Safe Rust guarantees memory safety — no undefined behavior from memory misuse.
- You can use unsafe Rust inside an
unsafe {}
block if needed. - Rust understands how to free memory even in:
- Loops
if
/match
clauses- Iterators
- Partial moves from structs
- ✅ If your program compiles, you get these memory safety guarantees without a runtime garbage collector.