Learning Rust · Part III · Chapter 02

Ownership, formally

Part II told you the story of ownership with box diagrams. This chapter cashes it in rigorously: what a move actually does to the bits, when a value is destroyed, which types escape moving entirely, and the small set of rules the borrow checker is really enforcing.

01

One value, one owner, and what a move really is

In most languages you have written, assigning one variable to another is a quiet, cheap thing you never think about. In Python or Java, b = a makes b point at the same object a already points at: now two names share one thing. In Go, b := a copies the value, and if it is a slice or a map you get a second header pointing at the same backing store. Either way, after the line runs, a is still usable, and you rarely ask who is responsible for freeing the thing they share.

Rust answers that last question first, and the answer shapes the whole language. Every value has exactly one owner: one binding responsible for it, and when that binding goes away the value is destroyed. Part II drew this for you as a box with a name on it. This chapter is where the box becomes mechanical. Run this:

the_move.rs
// A value has exactly one owner. Assigning it moves ownership to the new
// binding and invalidates the old one. There is no copy of the heap data.
fn main() {
    let original = String::from("ferris");
    let moved = original; // the value moves: `moved` is now the sole owner

    // `original` no longer owns anything, so it is gone. Only `moved` is live.
    println!("the value now lives in `moved`: {moved}");
    println!("there is exactly one owner, and it is `moved`");
}

The line let moved = original; is not a copy and not a shared reference. It is a move: ownership of the string is transferred from original to moved. At the machine level the few bytes that make up the String value itself (a pointer to the heap buffer, a length, and a capacity) are shifted into the new binding, and the old binding original is marked invalid by the compiler. The heap buffer holding the text ferris is not touched, not duplicated, not walked: it simply has a new owner. One value, one owner, before and after.

A move is a shift of ownership, not a deep copy

A move copies only the small, fixed-size value (for a String, three machine words: pointer, length, capacity) into the new binding, and invalidates the old one. The heap data it points at is never duplicated. This is why moves are cheap no matter how large the underlying buffer is: moving a one-gigabyte String shifts the same three words as moving a tiny one.

The reason the source binding has to be invalidated is the whole point. If both original and moved were still usable, both would believe they owned the same heap buffer, and at the end of the scope both would try to free it. Freeing the same memory twice is the classic double free bug, a stubborn source of crashes and security holes in C and C++. Rust makes it impossible by construction: after a move there is only one owner, so there is exactly one free.

02

Use after move is a compile error, not a crash

Here is where a beginner trips, so let us trip on purpose and read the result. If a move invalidates the source binding, then trying to use that binding afterward must be wrong. In a language with shared references this would be fine; in Rust it is a flat error. The program below does not compile. Press Run and read the diagnostic rather than fixing it:

use_after_move.rs
// This program does NOT compile. It is here so you can read the error.
// Press Run and look at the diagnostic: error[E0382].
fn main() {
    let original = String::from("ferris");
    let moved = original; // ownership moves out of `original` here

    // `original` was emptied by the move above, so touching it is a compile
    // error, not a runtime crash. Rust catches the use-after-move for you.
    println!("{original}");
    println!("{moved}");
}

The error is error[E0382]: borrow of moved value. The compiler points at let moved = original; and says value moved here, then points at println!("{original}") and says value borrowed here after move. It even tells you why the move happened in the first place: String does not implement the Copy trait, the trait that would have made the assignment a copy instead. We meet Copy properly in section 06; for now, notice only that the compiler caught a use-after-move and refused to build the program.

Compare that to what the same mistake costs elsewhere. In C++ a moved-from object is left in a valid-but-unspecified state, and reading it compiles cleanly and runs, handing you garbage or worse. In a garbage-collected language the question does not arise, but you pay for that with a runtime collector and no control over when memory is reclaimed. Rust takes a third road: the mistake is caught at compile time, with a specific error code and a finger pointing at the exact line, and it costs you nothing at runtime.

Why the error says "borrow" when you only read

The message is borrow of moved value even though you wrote a plain println!. That is because reading a value through {original} is, under the hood, a borrow of it, and you cannot borrow something that no longer owns anything. Do not read the word borrowas scary or as a separate feature yet: it is the subject of the very next chapter. Here it is just the compiler’s precise name for "you looked at a value that had already moved away."

03

Moves happen across function boundaries too

Assignment is not the only thing that moves a value. Passing a value to a function moves it in exactly the same way, and so does returning one. This is the same rule you just learned, applied at the call site instead of at an = sign. If you come from Go, this is the moment to slow down: in Go, passing a string or a struct to a function copies it, and you keep your own copy. In Rust, passing a non-Copy value gives it away. Run this and watch:

passing_ownership.rs
// Passing a value to a function moves it, exactly like assignment does.
// A function can keep the value (consume it) or hand it back (return it).
fn consume(message: String) {
    println!("consume received: {message}");
} // `message` is owned here, so it is dropped at the end of consume

fn refuel(message: String) -> String {
    println!("refuel took it, then returns it: {message}");
    message // ownership goes back to the caller
}

fn main() {
    let note = String::from("deploy at noon");
    consume(note); // ownership moves into consume; `note` is no longer usable

    let token = String::from("a1b2c3");
    let token = refuel(token); // moves in, then the return moves it back out
    println!("main still has the token because refuel returned it: {token}");
}

Two functions, two outcomes. consume(note) moves note into the function, and the function keeps it: after that call, note is gone in main, just as original was gone after the assignment in section 01. The second function, refuel, takes ownership and then returns the value, which moves it back out to the caller. The line let token = refuel(token); moves the token in, runs the function, and moves it back out into a fresh token binding, so main can keep using it.

That give-it-back dance, take ownership only to return it, is common enough that it should feel a little clumsy to you, and it should. Most of the time you do not actually want to own a value just to read or tweak it; you only want to look at it and let the caller keep it. Rust has a direct tool for exactly that, called borrowing, and it is the entire subject of the next chapter, III.03. For this chapter, stay with the strict picture: to use a value in a function the old way, you move it in, and if the caller still needs it, you move it back out.

Passing and returning are moves

A function call moves each non-Copy argument into the function, and a return moves the value back out to the caller. Ownership follows the value wherever it goes. So a function signature that takes a Stringby value is announcing, in the type, "hand this over; I am taking ownership."

04

A value is dropped the moment its scope ends

Ownership is only half the story; the other half is destruction. In a garbage-collected language you create objects and forget about them: some collector, at some unpredictable later time, reclaims the ones nothing references. In C you call free by hand and live with the consequences of forgetting. Rust takes neither route. A value is destroyed (the word Rust uses is dropped) at a single, predictable moment: when its owning binding goes out of scope. That is the closing brace } of the block it lives in, and nothing else.

To watch this happen you need a value that announces its own destruction. You can give a type that behavior by implementing the Drop trait, whose single method drop runs automatically right before the value is destroyed. We use it here purely as a print statement, a window into timing you would not otherwise see. Run it:

scope_and_drop.rs
// A value is dropped (destroyed) the moment its scope ends, deterministically.
// No garbage collector decides when; the closing brace does.
struct Guard {
    id: u32,
}

impl Drop for Guard {
    fn drop(&mut self) {
        println!("guard {} released", self.id);
    }
}

fn main() {
    let outer = Guard { id: 1 };
    println!("entered main, outer is live");

    {
        let inner = Guard { id: 2 };
        println!("entered block, inner is live");
        println!("using inner {}", inner.id);
    } // inner's scope ends here, so it is dropped right now

    println!("back in main, only outer remains");
    println!("using outer {}", outer.id);
} // outer is dropped here, at the end of main

Read the output against the source line by line. inner is declared inside the inner block, so its scope is that block, and the instant control reaches the inner closing brace you see guard 2 released, before main goes on to print back in main. outer lives for all of main, so it is dropped last, at the final brace, and you see guard 1 released dead last. The destruction is not vague or eventual: it is pinned to a specific brace you can point at in the source.

Deterministic destruction, no collector

Rust runs dropexactly when a value’s owner leaves scope: deterministic, and at a line you can see. There is no background collector and no free for you to forget. This is what lets Rust release memory, close files, and unlock locks at precise, known points without a runtime watching over your shoulder.

Drop is the destructor, not something you call

You implement Drop and write its drop method, but you almost never call drop yourself; the compiler inserts the call at the end of scope for you. In fact Rust forbids calling the method directly, precisely so a value cannot be destroyed twice. (There is a free function, std::mem::drop, for dropping a value early on purpose, but that is a tool for later, not a thing you reach for now.)

05

Drop order, and who is on the hook to drop

Once destruction is automatic, two questions follow. When several values share one scope, in what order do they drop? And when a value has moved, who actually drops it? Both have crisp answers, and both matter the instant your values hold something real (a file handle, a lock, a network connection) where the order of release is not cosmetic.

First, the order. Values in the same scope drop in the reverse order of their declaration: the last one declared is the first one dropped, like popping a stack. Run this and read the last two lines of output:

drop_order.rs
// When several values share a scope, they drop in REVERSE order of
// declaration: last declared, first dropped, like unwinding a stack.
struct Receipt {
    label: String,
}

impl Drop for Receipt {
    fn drop(&mut self) {
        println!("dropping {}", self.label);
    }
}

fn main() {
    let first = Receipt { label: String::from("first") };
    let second = Receipt { label: String::from("second") };
    println!("both built: {} and {}", first.label, second.label);
    // At the end of main: second drops first, then first. Reverse order.
}

first is declared before second, but second drops first and first drops last. The reason is the stack discipline you already know from how functions nest: later declarations may depend on earlier ones, so unwinding in reverse tears down a thing before the things it was built on top of. You rarely have to reason about this, but when a later value borrows from an earlier one (a chapter or two away), reverse drop order is what keeps the teardown sound.

Second, who drops a value that has moved. The answer falls straight out of the single-owner rule: the current owner drops it, at the end of thatowner’s scope. Move a value into a function and you have handed the function the duty to drop it. Run this:

who_drops_it.rs
// Moving a value into a function transfers the duty to drop it. The value
// is destroyed where it is finally owned, not where it was created.
struct Ticket {
    seat: u32,
}

impl Drop for Ticket {
    fn drop(&mut self) {
        println!("ticket for seat {} torn up", self.seat);
    }
}

fn redeem(ticket: Ticket) {
    println!("redeeming seat {}", ticket.seat);
} // `ticket` is owned here, so it drops at the end of redeem

fn main() {
    let t = Ticket { seat: 12 };
    println!("main owns the ticket");
    redeem(t); // ownership moves into redeem; main no longer owns it
    println!("back in main; the ticket was already dropped inside redeem");
}

The ticket is created in main but moved into redeem, so it is owned by redeem when the function ends, and that is where it drops: you see ticket for seat 12 torn up printed before main’s final line, not after. main never drops the ticket, because by then main does not own it. Ownership carries the obligation to clean up, and the obligation travels with the value on every move.

Move transfers the duty to drop

Whoever owns a value when its scope ends is the one who drops it. A move hands over not just the value but the responsibility for destroying it, which is exactly why there is never a double free: only one binding is ever the owner at the end, so drop runs exactly once.

06

Copy types: when assignment does not move

By now you might wonder whether every assignment in Rust steals from the source binding. It does not, and the exception is one you have already been relying on without noticing. Some types are Copy: assigning one duplicates its bits and leaves the source perfectly valid, so no move occurs at all. Run this and notice that every source binding stays usable:

copy_scalars.rs
// Small scalar types are Copy: assigning them duplicates the bits, so the
// source binding stays valid. No move happens, and nothing is invalidated.
fn main() {
    let count = 7_i32;
    let same = count; // i32 is Copy, so this copies the four bytes
    println!("count is still usable: {count}, and same too: {same}");

    // A tuple of Copy types is itself Copy.
    let point = (3.0_f64, 4.0_f64);
    let echo = point;
    println!("point: {point:?}, echo: {echo:?}");

    let flag = true; // bool is Copy
    let mirror = flag;
    println!("flag: {flag}, mirror: {mirror}");
}

After let same = count;, both count and same print fine, because i32 is Copy: the assignment copied four bytes and invalidated nothing. The same holds for the f64 tuple and the bool. This is why the scalars from III.01 never gave you a move error: they were quietly copying the whole time.

Which types are Copy? The rule is small and principled: a type is Copy when duplicating its bits is a complete and honest duplication of the value, with nothing owned elsewhere that a copy would alias. That covers the integer types (i32, u8, usize, and the rest), the floats (f32, f64), bool, char, and tuples and arrays built only from Copy types. It deliberately does not cover String, because a String owns a heap buffer, and copying its three words would make two values point at one buffer: an alias, and eventually a double free. That is precisely the situation the move rule exists to prevent, so String moves instead.

Copy means no move occurs

For a Copy type, assignment, passing to a function, and returning all copy the value rather than moving it, so the source binding stays valid. Copy types are exactly the small, self-contained values that own no heap data: numbers, bool, char, and tuples or arrays of those. If a type owns something on the heap, it is not Copy, and it moves.

Copy is a property the compiler checks, not magic

A type is Copy only if every one of its parts is Copy; you cannot make a type with a String field Copy even if you wanted to, and the compiler will tell you so. The flip side is that being Copy never deep copies a heap buffer behind your back, because Copy types have no heap buffer to copy. The expensive duplication is always spelled out explicitly, which is the next section.

07

Clone: opting in to a deep copy

Sometimes you genuinely want a second, independent String (or any other move type), heap buffer and all, with the original left usable. Rust will not do that silently, because duplicating a heap buffer is real work and real memory, and the language refuses to hide costs that size. Instead you ask for it by name, with .clone(). Run this:

clone_opt_in.rs
// String is NOT Copy, because copying it would mean duplicating a heap
// buffer, which is never silent in Rust. To opt into a deep copy, call clone.
#[derive(Clone)]
struct Config {
    name: String,
    retries: u32,
}

fn main() {
    let base = Config { name: String::from("prod"), retries: 3 };

    // .clone() builds a second, fully independent value, including a fresh
    // heap buffer for the String. Now there are two owners, one per binding.
    let copy = base.clone();

    println!("base:  name={}, retries={}", base.name, base.retries);
    println!("copy:  name={}, retries={}", copy.name, copy.retries);
    println!("both bindings are alive because clone duplicated the value");
}

let copy = base.clone(); builds a brand-new Config with its own freshly allocated heap buffer for the name string, so afterward base and copy are two fully independent values, each its own owner, both usable. No move happened: base is still alive, because clone read it without taking it. The #[derive(Clone)] line above the struct is what taught Config how to clone itself, by cloning each of its fields in turn.

It is worth being precise about the difference between the two operations, because the names are close and the behaviors are not. Copy is implicit and free-ish: it happens on a plain assignment, only for types with no heap data, and it never costs more than copying a few bytes. Clone is explicit and possibly expensive: you call .clone() on purpose, it can duplicate a heap buffer of any size, and it is the only way to deep copy a move type. Every Copy type is also Clone (cloning it is just the cheap copy), but the reverse is false: String is Clone but emphatically not Copy.

Copy is the cheap implicit one; Clone is the explicit one

Copy duplicates bits automatically for small, heap-free values, with no syntax and no cost worth naming. Clone is a method you call by hand, .clone(), that can perform an arbitrarily deep copy, allocating new heap memory as needed. The visible .clone()in the source is Rust’s way of keeping every expensive duplication honest and out in the open.

clone is not a fix to sprinkle on move errors

When you hit an E0382 use-after-move, the compiler often suggests .clone(), and it does make the error go away. But reach for it knowing what it costs: a clone allocates and copies, and scattering clones to silence the borrow checker quietly turns a zero-copy program into a copy-happy one. Often the value you wanted was not a second owner at all but a borrow, which is free. That is the tool the next chapter hands you, and it is usually the right answer when a clone feels like a workaround.

08

The rules the borrow checker enforces, stated plainly

Everything in this chapter is the compiler enforcing a short list of rules, the part of the compiler called the borrow checker. You have now seen each rule in action, so here they are written down, the full set for this chapter, with borrowing itself held back for III.03. Each owned value obeys these:

One, every value has exactly one owner at any moment: one binding responsible for it. Two, assigning or passing a non-Copy value moves it, transferring ownership and invalidating the source binding, so a use after move is a compile error (E0382). Three, a value is dropped when its owner goes out of scope, deterministically, with values in one scope dropping in reverse declaration order. Four, the owner at end of scope is the one who drops, which is why ownership can move freely and yet drop still runs exactly once, never twice and never zero times.

Read those four together and you can see they are not four separate features but one idea viewed from four sides. Single ownership makes destruction unambiguous; moves keep ownership single as values travel; scope-based drop makes destruction automatic and timely; and the owner-drops rule ties the knot, guaranteeing one free per value with no collector and no manual free. Copy and Clone are the two ways out: Copy for the small values where duplication is harmless and implicit, Clone for the deliberate, visible deep copy.

One thread runs ahead of here on purpose. Over and over you bumped into the fact that to merely look at a value in a function, the strict rules of this chapter make you move it in and move it back out. That is almost never what you want, and the fix is borrowing: taking a temporary reference to a value without taking ownership of it, so the owner keeps the value and you still get to use it. That is III.03, Borrowing and References, and it is the natural next step from everything here.

The whole chapter in six lines

  1. One value, one owner. Every value has exactly one binding responsible for it, and that owner is who frees it.
  2. A move shifts ownership, not heap data. Assigning, passing, or returning a non-Copy value moves the few words of the value and invalidates the source binding; the heap buffer is untouched.
  3. Use after move is a compile error (E0382), caught before the program ever runs, which is how Rust rules out the double free.
  4. Values drop at end of scope, deterministically and in reverse declaration order, and the current owner is the one who drops, so drop runs exactly once.
  5. Copy types do not move: numbers, bool, char, and tuples or arrays of those copy their bits on assignment and leave the source valid.
  6. Clone is the explicit deep copy you call by hand with .clone() when you really do want a second independent owner, heap buffer and all.
End-of-chapter assessmentCheck your understanding20 questions. Your first attempt is recorded on your report card.Take it →