Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Predict Copy, Move, and Drop Behavior in Assignments and Function Calls

Hello! In the previous lesson, you traced which variable owns a value and what happens when that owner leaves scope. Now we can make a more precise prediction: when a value is assigned or passed to a function, does Rust copy it, move it, or drop an existing value? This distinction will help you read compiler errors and design functions for your eventual backend application.

The central question is not “Is this variable on the stack?” It is “Does this type implement Copy?” We will build that rule from familiar integers and String, then apply it to function calls, returns, and reassignment.


Assignment: copying a value or moving ownership

Consider two assignments that look almost identical:

let x = 5;
let y = x;

let first = String::from("ready");
let second = first;

After let y = x, both x and y are usable. An integer such as i32 implements Rust’s Copy trait, so the assignment creates a second, independent integer value.

After let second = first, only second is usable. String does not implement Copy: ownership of its value has moved to second. Trying to use first now produces a compile-time error.

The distinction matters because a String contains a small, fixed-size representation—including a pointer, length, and capacity—while its text is held in a heap allocation. Merely duplicating that representation would leave two apparent owners of the same allocation. If both tried to free it, the result would be a double free. Rust instead treats the original binding as invalid and lets the new owner clean up the allocation once.

A `String` move transfers ownership of the existing text buffer from `s1` to `s2`. The pointer, length, and capacity describe that buffer; the text itself is not duplicated, and `s1` can no longer be used.

Read the relevant explanation in The Rust Programming Language. Its diagrams add detail to the ownership transfer shown above.

What is Ownership? - The Rust Programming Language

Read the Rust book’s account of String moves and replacement to see both when an old binding becomes unusable and when an old value is actually dropped.

In “Variables and Data Interacting with Move,” begin where the authors say that variables interact and continue through the explanation following Figure 4-4. Focus on why s1 becomes invalid even though the heap text has not been copied or freed. Then read “Scope and Assignment,” from the example beginning replacing s through the explanation of when the original string is freed. Stop before “Variables and Data Interacting with Clone.”

A move is an ownership rule, not a promise about particular machine instructions. Rust may copy some bits of a value while implementing a move, or optimize those operations away. What you can rely on is the source-level result: after a move, the old binding cannot be used to access that value.


Predicting which types copy

Copy means that an ordinary assignment may implicitly duplicate a value while leaving the source usable. Integers, floating-point numbers, Booleans, and characters implement Copy. A tuple implements it if all its elements do. For example, (i32, bool) is Copy, whereas (i32, String) is not.

Do not turn this into the rule “stack values copy; heap values move.” Even a struct containing only integers moves by default unless its type implements Copy:

struct Position {
    row: i32,
    column: i32,
}

fn main() {
    let a = Position { row: 2, column: 4 };
    let b = a;

    // Using a here would be an error: Position does not implement Copy.
    println!("({}, {})", b.row, b.column);
}

This struct is eligible to implement Copy, but eligibility is not the same as actually implementing it. Adding #[derive(Clone, Copy)] above struct Position would make let b = a copy the position and leave a usable. You do not need to implement traits yourself yet; for now, recognize that the type’s Copy implementation determines the rule.

String offers a different operation when you genuinely need two independent strings:

let first = String::from("ready");
let second = first.clone();

println!("{first} / {second}");

Here clone() explicitly duplicates the text into a separate allocation. Both values are valid, and each will be cleaned up separately. Unlike an implicit Copy, a clone may perform substantial work; it should express an actual need for another owned value, rather than become an automatic response to every move error.


Function calls follow the same rule

A function parameter is another place to receive a value. Passing an argument by value applies the same copy-or-move rule as assignment:

fn show_number(value: i32) {
    println!("{value}");
}

fn consume_message(message: String) {
    println!("{message}");
}

fn main() {
    let count = 3;
    show_number(count);
    println!("{count}"); // Fine: i32 was copied into value.

    let message = String::from("queued");
    consume_message(message);
    // println!("{message}"); // Error: message moved into the function.
}

When consume_message returns, its parameter message leaves scope and the String it owns is dropped, freeing its heap allocation. The caller’s message is not dropped a second time: that binding stopped owning the value at the call.

Returning a value works in the other direction. In the next example, the function receives ownership and then returns it rather than dropping it:

fn hand_back(message: String) -> String {
    message
}

fn main() {
    let original = String::from("queued");
    let returned = hand_back(original);

    // original is invalid; returned owns the String.
    println!("{returned}");
} // returned is dropped here.

The Rust book presents function arguments and returns together. Read its examples before using the prediction rule below.

What is Ownership? - The Rust Programming Language

Continue in the same Rust book chapter to connect a type’s Copy behavior with what happens at a function boundary.

In “Stack-Only Data: Copy,” start with the integer explanation and continue to the end of that subsection, including the examples of Copy types and the tuple qualification. In “Ownership and Functions,” begin with function transfers. Follow Listing 4-3 and, under “Return Values and Scope,” Listing 4-4. Stop before the tuple-return workaround in Listing 4-5.

You may notice that println!("{returned}") can display a String without consuming it. Formatting uses the value without taking ownership; the next lesson will explain the underlying mechanism, borrowing. For this lesson, focus on calls whose parameters explicitly take values such as String or i32.


When does a value get dropped?

A move and a drop are different events. A move makes the source binding unusable and transfers responsibility for the value. A drop ends an owned value’s lifetime; for a String, its drop also releases the text buffer. Two common drop points are worth distinguishing:

  1. The current owner leaves scope without transferring the value onward. This is what happens to message at the end of consume_message.
  2. A new value replaces an existing value. Rust drops the old value as part of the replacement rather than waiting for the variable’s scope to end.
fn main() {
    let mut status = String::from("queued");
    status = String::from("running"); // The old "queued" String is dropped.

    let final_status = String::from("done");
    status = final_status;            // The old "running" String is dropped.
                                      // final_status moves into status.

    println!("{status}");
} // The "done" String, now owned by status, is dropped.

A useful way to trace this code is to keep two questions separate: Which binding may I use next? and Which binding currently owns a value that will need cleanup? After status = final_status, final_status is unusable, but its former value has not been dropped. status now owns it.

For Copy values, replacement follows the same idea of a new value taking the old value’s place, but there is ordinarily no heap allocation or other resource to clean up. Do not infer from the presence of = alone that an expensive deallocation occurs.


A compact prediction procedure

When you encounter an assignment or a by-value function call, use this sequence:

  1. Identify the value’s type. Do not decide from the variable’s name or from whether its representation fits on the stack.
  2. Ask whether that type implements Copy. If so, the source remains usable. Otherwise, the value moves and the source cannot be used until it receives a new value.
  3. Identify the recipient. It might be a new binding, an existing binding, or a function parameter. If an existing binding already owns a value, replacing it drops that old value.
  4. Follow the resulting owner. If ownership is returned, it continues in the caller. Otherwise, the value is dropped when its owner’s lifetime ends.

This also explains why an i32 and a String passed to the same-looking function signature can lead to different outcomes: the call syntax does not decide the rule; the argument type does.


Conclusion

Assignments and by-value function calls copy values whose types implement Copy; otherwise, they move ownership and make the source binding unusable. A moved value is not thereby dropped. It is dropped when its current owner finishes with it, while an existing value can be dropped earlier when its binding is overwritten. clone() is the explicit choice when you need a second independent owned value.

Returning ownership from every function would be cumbersome when a function only needs to inspect something. In the next lesson, you will use immutable references to let a function access a value without taking ownership.

Can't find a good explanation? Sign up and we'll make it for you

Sign up