π§ 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 manualfree
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 β seeheap-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.
Top comments (0)