---
title: 'Ownership and Moves'
source: 'https://academia.sh/en/courses/ownership/ownership-and-moves'
course: 'Ownership and the Memory Model'
language: en
updated: '2026-09-15T04:58:41+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Shows by running it that moving a value into a function, an inner scope, and a struct field all fall into the where-it-was-moved class'
    - 'Explains why the old binding cannot be used after a move and proves that the new owner can use the value'
    - 'Verifies by measurement that a move is not a copy — the ledger gets exactly one record per value'
---

# Ownership and Moves

Moving a value into a function, into an inner scope, and into a struct field all fall into the same class (where it was moved), and in all three the value ends exactly once; that the old binding cannot be used after a move is a rule the compiler tests at compile time, while the new owner can use the value without any problem.

The previous lesson measured how a value ends at scope end and only touched on the
where-it-was-moved class through three examples. This lesson deepens that class: when
ownership passes from one place to another, exactly what happens to the ending point?
The question the Concurrency course asks — how a goroutine stops — looks similar but is
separate: there, a running unit stops; here, a value ends.

The Programming Fundamentals course established as a concept how a value passes to a
function — passing by value; that course explained this form of passing through Python,
and showed that a value passed into a function does not affect the name in the caller's
hands. What this lesson measures is that Rust loads a completely different meaning onto
the same syntax (writing a value as an argument to a function): a value passed to a
function now belongs to the function too, and the name in the caller's hands loses its
validity. This change of ownership is tested at compile time, not at runtime. Accessing
a value through a reference without giving up ownership is a separate path — that path
is taken up in the Borrowing lesson.

## Three Forms of Moving

The same class (where it was moved) can be entered through three separate paths. A
value can be passed as an argument to a **function**; a value can be rebound to a new
name in an **inner scope** opened inside the same scope; a value can be placed into a
**struct field**. What all three have in common is that the old binding no longer holds
the value — the value stays the same, only which name reaches it changes.

**OR4.** Binding a value to a new name in an inner scope (`let new_owner = inner;`)
should not be confused with the shadowing from the previous lesson. Shadowing created a
**new value** under the same name, and the old one went on living separately until its
own scope end. Here the exact opposite happens: **the same value** moves to a different
name, and the old name can no longer reach that value. Both look like a kind of
"rebinding," but one starts a new life, while the other changes the owner of a life
that already exists.

Although the three forms appear with different syntax, they are three use sites of one
single mechanism: in Rust, writing a value of a type not marked `Copy` anywhere — into a
parameter, a variable, a field — **moves** that value from there to there. The syntax
always looks the same (an `=` sign, a call), but the meaning behind it is the same every
time: the bit pattern at the old location moves to the new location, and the old
location no longer counts as valid. This lesson measures that all three separate places
(a parameter, an inner-scope binding, a struct field) reduce to this one single
mechanism — not three different rules, but three faces of one rule.

In practice, the three forms also correspond to different design decisions. Moving a
value into a function is preferred when the function is meant to have the final word
over that value — once the function ends, the value ends too, and the caller has no
more business with it. Rebinding a value in an inner scope shows up when the same value
needs to be kept under a different name, usually for a narrower, temporary operation.
Placing a value into a struct field, on the other hand, permanently makes the value part
of a larger whole — from then on, the value's life is tied not to its own scope but to
that struct's life. All three use the same mechanism, but the choice depends on which
unit (function, scope, struct) will draw the value's new boundary.

This choice mostly falls out of the data model itself. A program processing a record
that moves it into a function that validates it (moving into a function) expects the
record to stay there once validation ends — the caller side will not use the record
again anyway. If the same program wants to hold a record temporarily in a transformed
form (moving into an inner scope), the transformation stays valid only in that narrow
scope. If a record is going to be part of a persistent structure (a list or a cache, for
example — moving into a struct field), ownership is handed to that structure from the
start. The choice among the three forms is determined, while writing code, by the
answer given to the question "who does this value belong to now."

## The Measurement Excerpt

Only the `Trace` type, the `Drop` implementation, and the observation-ledger excerpt are
taken from the previous lesson's core; sections A, C, and D are not needed in this
lesson. The helper part is placed in a separate file so that the three forms of moving
can be seen side by side in a single flow.

The helper file does not compile on its own — `trace.rs` has no entry point, so it
cannot be run; it is only written to the working directory. The `mod trace;` line at the
very top of `moves.rs` pulls this file into the main compilation; `use trace::{Trace,
new_ledger, read_log};` then states which names from it will be used directly. The
`Trace` struct's `name` field and the `new`/`new_ledger`/`read_log` functions are marked
`pub` so they can be reached from outside the module; the `ledger` field is not marked,
because only `Trace`'s own methods reach it. Marking visibility this way, item by item,
was already established in the Setup and the Package Manager lesson; here it is only one
application of it.

```rust
// trace.rs — a measurement helper that records a value's ending
use std::cell::RefCell;
use std::rc::Rc;

pub type Ledger = Rc<RefCell<Vec<String>>>;

pub struct Trace {
    pub name: String,
    ledger: Ledger,
}

impl Trace {
    pub fn new(name: &str, ledger: &Ledger) -> Trace {
        Trace { name: name.to_string(), ledger: Rc::clone(ledger) }
    }
}

impl Drop for Trace {
    fn drop(&mut self) {
        self.ledger.borrow_mut().push(self.name.clone());
    }
}

pub fn new_ledger() -> Ledger {
    Rc::new(RefCell::new(Vec::new()))
}

pub fn read_log(ledger: &Ledger) -> Vec<String> {
    ledger.borrow().clone()
}
```

```rust
// moves.rs — three forms of moving: into a function, into an inner scope, into a struct field
mod trace;
use trace::{Trace, new_ledger, read_log};

struct Container {
    content: Trace,
}

fn consume(received: Trace) {
    println!("name read inside the function: {}", received.name);
}

fn main() {
    let ledger = new_ledger();

    let moved_in = Trace::new("into-function", &ledger);
    consume(moved_in);
    println!("ledger after the function returns: [{}]", read_log(&ledger).join(", "));

    let inner = Trace::new("inner-scope", &ledger);
    {
        let new_owner = inner;
        println!("name read in the inner scope: {}", new_owner.name);
    }
    println!("ledger after the inner scope ends: [{}]", read_log(&ledger).join(", "));

    let field_value = Trace::new("into-struct", &ledger);
    {
        let container = Container { content: field_value };
        println!("name read in the struct field: {}", container.content.name);
    }
    println!("ledger after the struct scope ends: [{}]", read_log(&ledger).join(", "));

    let records = read_log(&ledger);
    println!();
    println!("total records: {}", records.len());
    for name in ["into-function", "inner-scope", "into-struct"] {
        let count = records.iter().filter(|k| k.as_str() == name).count();
        println!("{:<16}{} record", name, count);
    }
}
```

```
name read inside the function: into-function
ledger after the function returns: [into-function]
name read in the inner scope: inner-scope
ledger after the inner scope ends: [into-function, inner-scope]
name read in the struct field: into-struct
ledger after the struct scope ends: [into-function, inner-scope, into-struct]

total records: 3
into-function   1 record
inner-scope     1 record
into-struct     1 record
```

All three lines follow the same pattern: the value is first **read** in its new owner
(`received.name`, `new_owner.name`, `container.content.name`), then drops into the
ledger once the new owner's own scope closes. The moment `consume`'s body ends,
`into-function` is written to the ledger right away — the function's own scope becomes
the moved value's new and final scope. The same holds for `new_owner` in the inner
scope and `container.content` in the struct field: both end the moment their own narrow
scope closes, without waiting for the outer scope to close.

The manner of access also differs across the three forms. The value moved into a
function is reached directly by the parameter name (`received`), the value moved into
an inner scope by the new binding's name (`new_owner`), but the value moved into a
struct field no longer has a name of its own — it can only be reached through
`container.content`, through the struct that owns it. This third form shows that a move
is not just "copying one variable into another": once a value becomes **part of** a
struct, it loses its own independent name and becomes visible only through the field
path of the struct enclosing it.

## Why the Old Binding No Longer Works

The code never reads the old name (`moved_in`, `inner`, `field_value`) again after these
three moves — because it cannot. If a line trying to reach `moved_in.name` were added
after the `consume(moved_in)` call, the compiler would reject it: ownership passed to
`consume`'s parameter, and `moved_in` no longer points to a valid value. The same rule
applies to `inner` and `field_value`.

**OR5.** This rejection is not a runtime error, it is a rule tested at compile time.
Source code trying to read a moved name again does not compile at all; the program
never reaches that line, so the wrong usage costs nothing at runtime. This also explains
why the accepted code above (the three blocks) never reads the old name again anywhere —
correct code already stays inside this constraint.

This rejection comes from the same place as the previous lesson's single-owner
observation: two separate names being able to reach one value at the same time would
make it unclear which name's scope closing would end that value. The compiler prevents
this by treating the old name as **invalid** the moment the move happens — the old name
still sits in the source, but it can no longer be used.

A single thought experiment is enough to make concrete what this constraint prevents: if
`moved_in` stayed valid after the move, both `moved_in` and `received` could reach the
same `Trace` instance. Both would try to end that instance once their own scope closed,
and the same value would end up being dropped twice — the same name would be written to
the `ledger` twice. The compiler never lets this situation arise; at the moment of the
move, it makes sure only one name stays valid, and leaves the rest (when the ending
happens) to the previous lesson's rules.

## Moves Can Be Chained

The three forms each showed a single move, but moving can also happen one after
another: a value can enter a function and be handed from there to another function,
even back to the caller. The example below measures the same `Trace` value crossing two
separate function boundaries and ending in a third scope.

```rust
// chain.rs — a move can be chained: ownership can be handed from one function to another
mod trace;
use trace::{Trace, new_ledger, read_log};

fn second_stage(value: Trace) -> Trace {
    println!("name read in the second stage: {}", value.name);
    value
}

fn first_stage(value: Trace) {
    let handed_off = second_stage(value);
    println!("name back in the first stage: {}", handed_off.name);
}

fn main() {
    let ledger = new_ledger();
    let value = Trace::new("chained", &ledger);
    first_stage(value);
    println!("ledger after the chain ends: [{}]", read_log(&ledger).join(", "));
}
```

```
name read in the second stage: chained
name back in the first stage: chained
ledger after the chain ends: [chained]
```

The value here passes through the hands of four separate names: `value` in `main`,
`first_stage`'s parameter, `second_stage`'s parameter, and `handed_off`, which holds the
value `second_stage` returns. Each pass is a move, and after each move the previous name
is no longer valid. At the end of the chain the value belongs to `handed_off`, and it
ends there once `first_stage`'s body ends — not in `main`. Four names, one value, one
ending: only `chained` appears in the ledger, regardless of how many times it was moved.

This shows why the where-it-was-moved class is defined not by a count but by a
**position**. How many times the value changed hands does not matter; what is measured
is only the final owner's scope. `second_stage` returning the value is a move too — the
return value moves into a new binding on the caller's side, which is why `handed_off`
is itself the result of a move that falls to its own share.

A function returning a value it owns is a pattern this course will meet again. When a
function wants to hand back to its caller a value it built inside itself or received as
a parameter, the option is not a reference but an **ownership transfer** — the value is
written as the function's return type, and the return counts as a move in its own
right. This pattern is also needed to understand, in the Borrow Checker lesson, why a
function returning a reference to a value it built inside itself gets rejected: what
gets rejected is exactly this — returning a reference; what gets accepted is what is
seen here — returning the value itself.

## Boundary Measurement: A Move Is Not a Copy

The output's last three lines show this directly: `into-function`, `inner-scope`, and
`into-struct` each appear in the ledger exactly **once**. Even if a value moved through
three separate names in sequence (it does not here, but suppose it did), each move
would not create a copy — the same `Trace` instance, the same memory location, only the
access name changes.

**OR6.** All three forms of moving produced exactly one record in the ledger; this
shows that the same value being dropped twice is impossible at the source level.
Because ownership always sits in a single name, the ending point is always triggered in
a single place too — a move does not break this singularity, it only changes which name
is responsible.

That a move does not **shorten** a life is also read from the same measurement.
`into-function` is the same `Trace` instance both before and after being moved; a move
is not a re-creation, it is a re-naming. This is why "where it was moved," out of the
four classes seen in the previous lesson, is a class in its own right: the value
neither waits for scope end nor receives an explicit call, it only inherits its owner's
own ending.

Thinking through the opposite case makes the difference clear. If `Trace` values
implemented `Clone` and the three lines had been written as
`consume(moved_in.clone())`, `let new_owner = inner.clone();`, the ledger would have six
records: the three original values ending in their own scopes, and three copies ending
separately in theirs. `clone` was never called in this lesson — a move does not build a
new `Trace` instance, it hands the existing one from hand to hand. The ledger's three
records, not six, are direct proof of this difference.

This difference shows up not only in the record count but also in the nature of the
work. A copy requires building the copy's own data separately — for `Trace`, allocating
a new `String`; a move, in contrast, amounts to no more than assigning a new owner to
existing data, with no new allocation at all. This is why the four changes of hands
seen in the chaining example all happened on a single `String` — not four separate
copies, but four separate owners of the same data.

## Summary

- A value can be moved three ways — into a function, into an inner scope, into a
  struct field — and all three fall into the same class (where it was moved).
- A moved value can be used in its new owner; the old name, in contrast, can no
  longer be read after the move, because it no longer points to a valid value.
- The old name becoming unusable is a rule tested at compile time; it has no cost at
  runtime.
- A move is not a copy: the ledger gets exactly one record per value, and the same
  value is never dropped twice.
- A move can be chained — a value can cross several function boundaries one after
  another — but no matter how many times it changes hands, there is only one ending
  point at the end.

## Next Step

What the three forms of moving had in common was that the old name lost the value —
ownership went exactly from one place to one place each time. The next question
investigates exactly the opposite: is it possible to reach a value without ever giving
up ownership? The Borrowing lesson measures that the name lending a value out does not
lose the value, and that a borrowed value never ends anywhere during the borrow —
unlike a move, here the old owner remains the owner.
