Frontend Labs
⋯

6. Ownership: moves, copies, and drop

This chapter is the core of the language. Read it slowly.

6.1 The three rules

  1. Each value has exactly one owner (a variable, a struct field, a Vec slot...).
  2. When the owner goes out of scope, the value is dropped: its destructor runs and its heap memory is freed.
  3. Ownership can be moved to a new owner. After a move, the old owner cannot be used.

That is the complete memory-management model. There is no GC, and no free for you to forget or call twice.

6.2 What is on the stack and what is on the heap

fn main() {
    let n: u64 = 42;                       // 8 bytes, on the stack
    let s = String::from("hello");         // 24 bytes on the stack: (ptr, len, cap)
                                           // + 5 bytes on the heap: h e l l o
    let v: Vec<u32> = vec![1, 2, 3];       // same shape: (ptr, len, cap) -> heap [1,2,3]
    println!("{n} {s} {v:?}");
    println!("{}", std::mem::size_of::<String>()); // 24 on a 64-bit target
}   // <- `v` is dropped, then `s` (reverse declaration order): both heap buffers are freed here.
 stack frame of main                 heap
 ┌─────────────────────┐
 │ n   = 42            │
 │ s   ptr ────────────┼──────────▶  [h][e][l][l][o]
 │     len = 5         │
 │     cap = 5         │
 │ v   ptr ────────────┼──────────▶  [1][2][3]
 │     len = 3, cap = 3│
 └─────────────────────┘

This is the same layout as the C guide's dynamic array and Go's slice header. The difference is what happens when you copy the stack part.

6.3 Move: copying the header transfers ownership

// DOES NOT COMPILE
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;              // the 24-byte header is copied; ownership MOVES to s2
    println!("{s1} {s2}");    // s1 is no longer valid
}
error[E0382]: borrow of moved value: `s1`
  |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
  |     let s2 = s1;
  |              -- value moved here
  |     println!("{s1} {s2}");
  |               ^^^^ value borrowed here after move
  = help: consider cloning the value if the performance cost is acceptable: `.clone()`

Why the compiler rejects this: if both s1 and s2 were valid, both would free the same heap buffer when they went out of scope. That is a double free, C guide §10.2. Go avoids it with a GC that keeps the buffer alive for both. Rust avoids it by allowing only one owner.

A move is only a memcpy of the stack part (24 bytes here). The heap data does not move. In optimized code the copy usually disappears too.

Passing a value to a function moves it, and returning a value moves it out:

fn consume(s: String) -> usize { s.len() }    // `s` is dropped at the end of consume
fn make() -> String { String::from("fresh") } // ownership moves out to the caller

fn main() {
    let a = String::from("data");
    let n = consume(a);          // `a` moved into consume; cannot use `a` after this line
    let b = make();
    println!("{n} {b}");
}

6.4 Copy and Clone

  • Copy types are duplicated bit for bit on assignment, and the original stays valid. These are the types with no heap ownership: integers, floats, bool, char, shared references &T, and tuples or arrays of Copy types. A type that implements Drop can never be Copy.
  • Clone is an explicit, possibly expensive deep copy: s.clone() allocates a new heap buffer.
#[derive(Debug, Clone, Copy)]
struct Point { x: i32, y: i32 }   // all fields are Copy, so the struct can derive Copy

fn main() {
    let p1 = Point { x: 1, y: 2 };
    let p2 = p1;                   // a copy, not a move
    println!("{p1:?} {p2:?}");     // both still valid

    let s1 = String::from("hi");
    let s2 = s1.clone();           // explicit deep copy: a second heap allocation
    println!("{s1} {s2}");
}

Cloning is visible in the code, so it is easy to find in review. When you see .clone() in a hot loop, it is often the first thing to optimize (Ch 68).

6.5 Drop: deterministic destruction (RAII)

struct TempFile { path: String }

impl Drop for TempFile {
    fn drop(&mut self) {
        // Runs exactly once, when the owner goes out of scope, including on early
        // return or panic unwind. Rust's answer to Go's `defer` and C's `goto cleanup`.
        println!("removing {}", self.path);
        let _ = std::fs::remove_file(&self.path);
    }
}

fn work(fail: bool) -> Result<(), String> {
    let _tmp = TempFile { path: "/tmp/rust-guide-demo".into() };
    std::fs::write("/tmp/rust-guide-demo", b"scratch").map_err(|e| e.to_string())?;
    if fail {
        return Err("bailed out early".into()); // _tmp is still dropped here
    }
    println!("finished work");
    Ok(())
} // ...and here on the normal path

fn main() {
    println!("{:?}", work(false));
    println!("{:?}", work(true));
}

Files, sockets, mutex guards, database transactions, and TLS sessions all release their resources in Drop. The C guide's §44.5 "every acquire needs a matching release" is automatic in Rust: you cannot forget the release, and you cannot run it twice.

Watch the binding name. let _guard = lock() keeps the guard alive until the end of the scope. let _ = lock() drops it immediately, because _ is not a binding. This is a real source of "my lock does nothing" bugs.

Leaks are still possible, and they are safe. Rc reference cycles (Ch 22) and std::mem::forget both leak memory. Rust guarantees no use-after-free, not no leaks. Ch 51 explains why.

6.6 War story

A team ported a C daemon to Rust. The C code had a conn_close(conn); log_conn(conn); sequence that read a freed struct on one error path. It had shipped for six years, because the freed memory usually still held the old bytes. The Rust port could not compile the same sequence: conn.close() took self by value, so conn was moved, and the logging line failed with E0382. The bug was found when the code was translated, not in production. Lesson: when an API consumes a value (fn close(self)), the compiler stops anyone from using that value afterwards. That is the typestate idea from Ch 62, in its simplest form.


Generated from real-life-rust-guide.md · 8 Oct 2026 16:36