Skip to content

Rust Ownership, Moves, Copy and Clone

Rust ownership determines which binding can use a value and when owned resources are released. This lesson uses a small text label to distinguish moving ownership, copying a value and explicitly cloning it; no C++ background is needed.

Prerequisites and outcome

Read types and expressions. By the end, predict which variable is usable after an assignment or function call. The next lesson introduces borrowing so a function can inspect text without taking ownership.

An owned String is not a string literal

String::from("Pi 4B") creates owned, growable text. The literal "Pi 4B" has type &str, a reference to string data. We use String here to study ownership; UTF-8 and text APIs receive fuller treatment later.

A value has an owner; assigning a non-Copy value to another binding transfers it. The old binding cannot then be used as though it still owned that value. This is a rule about valid use, not a promise that a particular number of machine instructions or physical memory copies occurs.

Moving this String does not request a duplicate of its text allocation. Do not use the shortcut “stack values copy, heap values move”: the type's Copy implementation determines copying, not a value's storage location.

Run a complete ownership example

cargo new pi_ownership
cd pi_ownership

Replace src/main.rs with:

fn decorate(label: String) -> String {
    // This function owns label and returns ownership of the result.
    format!("device={label}")
}

fn main() {
    let reading: i32 = 46_700;
    let copied = reading;
    println!("readings: {reading}, {copied}");

    let original = String::from("Pi 4B");
    let duplicate = original.clone();
    let moved = original;
    // original is no longer usable here; moved owns that String.
    println!("labels: {moved}, {duplicate}");

    let decorated = decorate(moved);
    println!("{decorated}");
}

#[cfg(test)]
mod tests {
    use super::decorate;

    #[test]
    fn owned_label_becomes_a_report() {
        assert_eq!(decorate(String::from("Pi 4B")), "device=Pi 4B");
    }

    #[test]
    fn string_clone_can_be_changed_independently() {
        let original = String::from("Pi");
        let mut duplicate = original.clone();
        duplicate.push_str(" 4B");
        assert_eq!(original, "Pi");
        assert_eq!(duplicate, "Pi 4B");
    }

    #[test]
    fn empty_owned_label_is_valid_input() {
        assert_eq!(decorate(String::new()), "device=");
    }
}
1
2
3
cargo run --quiet
cargo test
cargo fmt --check

Expected program output:

1
2
3
readings: 46700, 46700
labels: Pi 4B, Pi 4B
device=Pi 4B

Rust moves versus Copy and Clone

i32 implements Copy, so assigning reading leaves it usable. Copy is a type property, not a runtime choice made according to how large a value looks. Many simple types are Copy; String is not.

Operation What happens Old binding usable?
let copied = reading Copy an i32 Yes
let moved = original Move the String No
let duplicate = original.clone() Create independently owned String text Yes
decorate(moved) Transfer the String to the function parameter No

The clone occurs before the move. Reversing those statements attempts to clone after ownership transferred. The Copy API documentation also explains why types implementing Drop cannot implement Copy.

clone() explicitly requests duplication according to a type's Clone implementation. For String, the duplicate owns independent text storage. That does not mean every Clone implementation performs a deep copy: later, cloning an Rc shares ownership instead. Copy types also implement Clone.

Passing moved to decorate transfers ownership into its parameter. Returning the resulting String transfers ownership to the caller. The caller cannot reuse moved afterward. Prefer ownership transfer when the receiving function really needs to own a value; use borrowing for inspection rather than cloning just to silence an error.

format! builds a new String; it does not return the input allocation unchanged. The input is dropped when the function finishes, while the result belongs to decorated. The earlier println! borrows its arguments for formatting, so printing moved does not prevent the later ownership transfer.

Scope and resource release

An initialized owned value is normally dropped when its owner leaves scope. If it was moved, the old binding does not drop it again. Assigning a new value to an initialized owning variable also drops the old value; explicit drop(value) consumes a value early.

A name can remain in lexical scope after a move yet no longer be usable. A name declared in an inner block is inaccessible outside it even without a move. Reinitialising a moved mutable binding makes it own a new value; it does not restore the old value.

String's cleanup releases its owned allocation. Later we study Drop, resource wrappers and exception boundaries in detail. Do not generalise normal scope exit to process termination: aborts and intentional leaks do not provide the same cleanup guarantees.

The official ownership chapter explains the introductory model; destructors provide precise scope and cleanup rules.

Deliberately failing example

Use a separate scratch project for this complete program:

1
2
3
4
5
6
fn main() {
    let original = String::from("Pi 4B");
    let moved = original;
    println!("{original}"); // Error: use after move.
    println!("{moved}");
}

Run cargo check. Expect a moved-value error (E0382). Making original mutable does not restore ownership. Either use the new owner, clone when independent ownership is required, or redesign the operation to borrow, as the next lesson demonstrates.

Exercises and limits

  1. Call decorate(duplicate) twice: expect the second use to fail. Explain which call consumed the value.
  2. Put a temporary String inside a nested block. Use it inside the block, then try outside: expect a scope error, a different reason from use after move.
  3. Replace let moved = original with let moved = original.clone(): both bindings become usable. Explain why that changes the ownership relationship and may add allocation work.

The runtime tests check returned text and independent String cloning. The failing example checks compiler enforcement. Neither measures allocation cost or performance, and neither is a complete model of C++ move semantics; that comparison belongs in the advanced series.

Verification and next step

On October 10, 2026, this lesson was verified on a Raspberry Pi 4B with 64-bit user space, kernel 6.18.50+rpt-rpi-v8, Rust and Cargo 1.99.0, and edition 2024. Check, all 3 tests, formatting and debug/release output comparisons passed. The moved-value example and repeated-call and scope exercises failed for the expected reasons. The clone variant also compiled and ran. No hardware or allocation-cost benchmark is claimed.

Continue with references, borrowing and slices.

Previous: types and expressions · Course overview

Donate