DEV Community

Leon
Leon

Posted on

Rust: Ownership, Borrowing (References), Lifetimes, Move/Copy/Clone semantics

🧠 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
}
Enter fullscreen mode Exit fullscreen mode

Compiler Error:

error[E0515]: cannot return reference to local variable `number`
Enter fullscreen mode Exit fullscreen mode
  • 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
}
Enter fullscreen mode Exit fullscreen mode

πŸ’Ύ 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
}
Enter fullscreen mode Exit fullscreen mode

Compiler Error:

error[E0502]: cannot borrow `vec` as mutable because it is also borrowed as immutable
Enter fullscreen mode Exit fullscreen mode
  • 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);
}
Enter fullscreen mode Exit fullscreen mode

  • 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
}
Enter fullscreen mode Exit fullscreen mode

Compiler Error:

error[E0597]: `vec` does not live long enough
Enter fullscreen mode Exit fullscreen mode
  • 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);
}
Enter fullscreen mode Exit fullscreen mode

Compiler Error:

error[E0505]: cannot move out of `vec` because it is borrowed
Enter fullscreen mode Exit fullscreen mode
  • 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);
}
Enter fullscreen mode Exit fullscreen mode

πŸ”‘ 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
}
Enter fullscreen mode Exit fullscreen mode

❌ 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
}
Enter fullscreen mode Exit fullscreen mode

🧠 Compiler Error:

error[E0382]: borrow of moved value: `vec`
Enter fullscreen mode Exit fullscreen mode
  • 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
}
Enter fullscreen mode Exit fullscreen mode

⚠️ .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
}
Enter fullscreen mode Exit fullscreen mode
  • 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
}
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

βœ… Don’t implement Copy if you can live with using references.

βœ… You can also just implement Clone and call .clone() explicitly β€” see clone-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)