thecodex.expert · The Codex Family of Knowledge
Rust

Ownership and Borrowing

No GC, no manual free, no dangling pointers — the compiler tracks ownership and frees memory exactly when it should.

Rust 2024 edition Memory safe Last verified:
Canonical Definition

Rust ownership gives each value exactly one owner at compile time — when the owner goes out of scope the value is automatically freed; assignment moves ownership rather than copying; references borrow without taking ownership; and the borrow checker verifies at compile time that all references are valid.

The central idea

Rust's ownership system is the compiler checking that every value has exactly one owner at any time, and that references are always valid — eliminating use-after-free, double-free, and data races at compile time, with zero runtime cost.

Ownership rules

Rust has three ownership rules that the compiler enforces:

  1. Each value has exactly one owner.
  2. When the owner goes out of scope, the value is dropped (memory freed).
  3. There can only be one owner at a time — assigning moves ownership, not copies.
Rustownership.rs
fn main() {
    // s1 owns the String "hello" on the heap
    let s1 = String::from("hello");

    // Move: s1's ownership transfers to s2
    // s1 is no longer valid after this line
    let s2 = s1;
    // println!("{}", s1);  // ERROR: value borrowed after move

    // Clone: deep copy — both s2 and s3 are valid
    let s3 = s2.clone();
    println!("{} {}", s2, s3);   // "hello hello"

    // Types that implement Copy are copied, not moved
    // Primitives (i32, f64, bool, char) and tuples of Copy types
    let x = 5;
    let y = x;           // x is COPIED, not moved
    println!("{} {}", x, y);   // both valid: "5 5"

} // s2, s3 go out of scope here — drop() is called automatically
  // Memory freed without malloc/free or GC

Borrowing — references

Rustborrowing.rs
// & creates an immutable reference — you can read but not modify
fn print_length(s: &String) {   // borrows s, does not take ownership
    println!("Length: {}", s.len());
}   // s goes out of scope here but does NOT drop the value (we don't own it)

fn main() {
    let s = String::from("hello");
    print_length(&s);   // pass a reference — s still valid after
    print_length(&s);   // can borrow multiple times
    println!("{}", s);  // s still valid

    // Mutable reference — &mut — allows modification
    let mut s2 = String::from("hello");
    change(&mut s2);
    println!("{}", s2);  // "hello, world"
}

fn change(s: &mut String) {
    s.push_str(", world");
}

// THE BORROW RULES:
// 1. At any point, you can have EITHER:
//    - Any number of immutable references (&T), OR
//    - Exactly ONE mutable reference (&mut T)
// 2. References must always be valid (compiler enforces this)
// These two rules prevent data races at compile time

Slices

Rustslices.rs
fn main() {
    let s = String::from("hello world");

    // String slice — a reference to part of a String
    let hello = &s[0..5];    // "hello"
    let world = &s[6..11];   // "world"
    let all   = &s[..];      // the whole string

    // String literals are slices (&str)
    let literal: &str = "hello";  // &str — immutable slice into static memory

    // Functions should take &str to accept both String and &str
    fn first_word(s: &str) -> &str {
        let bytes = s.as_bytes();
        for (i, &byte) in bytes.iter().enumerate() {
            if byte == b' ' { return &s[0..i]; }
        }
        &s[..]
    }

    // Array slices
    let arr = [1, 2, 3, 4, 5];
    let slice = &arr[1..3];   // [2, 3] — type: &[i32]
    println!("{:?}", slice);
}

The borrow checker in action

Rustborrow_checker.rs
fn main() {
    let mut v = vec![1, 2, 3];

    // Cannot have mutable and immutable borrow at the same time
    let first = &v[0];          // immutable borrow
    // v.push(4);               // ERROR: mutable borrow while immutable exists
    println!("{}", first);      // immutable borrow used here
    // After this line, first is no longer used — borrow ends (NLL)

    v.push(4);                   // now OK — first borrow is done
    println!("{:?}", v);         // [1, 2, 3, 4]

    // Dangling reference — impossible in Rust
    // The compiler prevents returning a reference to a local variable
    // fn dangle() -> &String {
    //     let s = String::from("hi");
    //     &s          // ERROR: s goes out of scope at end of function
    // }              // would be a dangling pointer in C/C++

    // Box<T> — heap allocation with single ownership
    let b = Box::new(5);         // 5 on the heap
    println!("{}", b);           // auto-dereferenced
    // b is dropped here — heap memory freed automatically

    // Rc<T> — reference counted, multiple owners (single-threaded)
    use std::rc::Rc;
    let rc1 = Rc::new(String::from("shared"));
    let rc2 = Rc::clone(&rc1);   // increments reference count
    println!("{} (count: {})", rc1, Rc::strong_count(&rc1));  // count: 2
}   // rc1 and rc2 dropped — count reaches 0 — memory freed

Interior mutability: RefCell and Cell

Rustinterior_mutability.rs
use std::cell::RefCell;
use std::rc::Rc;

// RefCell<T> — enforces borrow rules at RUNTIME instead of compile time
// Useful when you need mutability inside an otherwise immutable context

fn main() {
    let data = RefCell::new(vec![1, 2, 3]);

    // borrow() — runtime immutable borrow (panics if mut borrow active)
    println!("{:?}", data.borrow());

    // borrow_mut() — runtime mutable borrow (panics if any borrow active)
    data.borrow_mut().push(4);
    println!("{:?}", data.borrow());   // [1, 2, 3, 4]

    // Common pattern: Rc<RefCell<T>> for shared mutable state
    let shared = Rc::new(RefCell::new(vec![1, 2]));
    let shared2 = Rc::clone(&shared);

    shared.borrow_mut().push(3);       // mutate through one owner
    println!("{:?}", shared2.borrow()); // [1, 2, 3] — seen by all owners

    // For thread-safe shared mutation: Arc<Mutex<T>>
    use std::sync::{Arc, Mutex};
    let counter = Arc::new(Mutex::new(0));
}
Commonly confused
Borrow checker errors are not bugs to work around — they are catching real bugs. When the borrow checker rejects code that "feels correct," it is almost always preventing a use-after-free, a data race, or an invalidated iterator. The right response is to understand why the check fired, not to reach for unsafe or RefCell as a first resort.
clone() is explicit deep copy — not automatic. In Rust, assignment of heap-allocated types (String, Vec, Box) moves ownership. To get a copy, you must call .clone() explicitly. This is intentional: it makes expensive copies visible in the code. Stack-allocated primitives that implement Copy are copied silently.
&str and String are different types. String is an owned, heap-allocated, growable string. &str is a borrowed slice — a reference to string data somewhere in memory (stack, heap, or static). Function parameters should prefer &str (accepts both) over &String (only accepts String references).

Non-Lexical Lifetimes (NLL) and the borrow checker algorithm

Rust's borrow checker was significantly improved in Rust 2018 with Non-Lexical Lifetimes (RFC 2094). Before NLL, a borrow lasted until the end of its lexical scope (the closing brace). With NLL, a borrow ends at the last point it is used. This eliminated many false positives where the borrow checker rejected code that was actually safe. The NLL algorithm models borrows as a region constraint problem — it computes the minimal span of code where each reference is live, then checks that no conflicting borrows overlap in their live ranges.

The ownership model and memory layout

Rust's memory model is explicit. The stack holds: local variables with known size, function call frames, and fixed-size data. The heap holds: dynamically sized data, accessed through pointers. A String is a struct of (pointer, length, capacity) on the stack pointing to data on the heap. When a String moves, the struct is bitwise-copied to the new location and the original is invalidated — no heap allocation happens. When a String is dropped, its heap memory is freed via the Drop trait, which generates the appropriate free call. There is no garbage collector — every allocation has a single, statically-known point of deallocation.

Sources

1
The Rust Programming Language — Ch 4: Understanding Ownership. doc.rust-lang.org/book/ch04-00-understanding-ownership.html.
2
Rust Reference — Ownership and Moves. doc.rust-lang.org/reference/expressions.html#moved-and-copied-types.
3
RFC 2094 — Non-Lexical Lifetimes. rust-lang.github.io/rfcs/2094-nll.html.
4
The Rust Programming Language — Ch 15: Smart Pointers. doc.rust-lang.org/book/ch15-00-smart-pointers.html.
Source confidence: High Last verified: Primary source: The Rust Book · doc.rust-lang.org/book/

Sources

1
The Rust Book — Ch 4: Ownership. doc.rust-lang.org/book/ch04-00-understanding-ownership.html.
2
RFC 2094 — Non-Lexical Lifetimes. rust-lang.github.io/rfcs/2094-nll.html.
3
The Rust Book — Ch 15: Smart Pointers. doc.rust-lang.org/book/ch15-00-smart-pointers.html.
4
Rust Reference — Moves. doc.rust-lang.org/reference/expressions.html#moved-and-copied-types.