6.2 Transferring Ownership: Move, Copy, and Clone

How data is handled during assignment or function calls depends on its type. Rust distinguishes between moving, copying, and cloning.

6.2.1 Move Semantics

Types that manage resources on the heap, like String, Vec<T>, or Box<T>, use move semantics by default. When ownership is transferred (either through assignment to another variable or by passing the value to a function), the underlying resource is not duplicated; only the “control” (ownership) moves. The original variable binding becomes invalid.

Move via Assignment:

fn main() {
    let s1 = String::from("allocated"); // s1 owns the string data on the heap
    let s2 = s1; // Ownership MOVES from s1 to s2. s1 is now invalid.
    // println!("s1: {}", s1); // Compile-time error! s1's value was moved.
    println!("s2: {}", s2); // s2 now owns the data. Prints: allocated
} // s2 goes out of scope, its owned string data is dropped.

Move via Function Arguments:

Passing a value to a function transfers ownership in the same way.

fn takes_ownership(some_string: String) { // `some_string` takes ownership of the passed value
    println!("Inside function: {}", some_string);
} // `some_string` goes out of scope, Drop is called, memory is freed.

fn main() {
    let s = String::from("hello"); // s comes into scope
    takes_ownership(s);           // s's value moves into the function...
                                  // ...and is no longer valid here.
    // println!("Moved string: {}", s); // Compile-time error! s was moved.
}

Move via Function Return Values:

Similarly, returning a value from a function moves ownership out of the function to the calling scope.

fn creates_and_gives_ownership() -> String { // Function returns a String
    let some_string = String::from("yours"); // some_string comes into scope
    some_string                              // Return some_string, moving ownership out
}

fn main() {
    let s1 = creates_and_gives_ownership(); // Ownership moves from function return to s1
    println!("Got ownership of: {}", s1);
} // s1 is dropped here.

What Actually Happens During a Move?

When a value like String (or Vec<T>, Box<T>) is moved – either through assignment (let s2 = s1;) or by passing it by value to a function (takes_ownership(s1);) – the operation is very efficient at runtime. Remember that a String value itself (the metadata) consists of a small structure holding {a pointer to the heap data, a length, a capacity}. This structure usually resides on the stack for local variables.

During a move:

  1. Bitwise Copy of Struct: The {pointer, length, capacity} structure is copied bit-for-bit from the source (s1) to the destination (s2 or the function parameter). This is a fast operation, similar to copying a simple struct in C. No heap allocation occurs for this structure itself; the bits are copied into the stack space already designated for the new variable or parameter.
  2. No Heap Interaction: The character data stored on the heap is not copied or modified. The pointer value that is copied simply points to the same heap allocation.
  3. Ownership Transfer: The responsibility for managing and eventually deallocating the heap buffer is transferred to the new variable/parameter.
  4. Invalidation: The original variable (s1) is marked as invalid by the compiler. Its destructor (Drop) will not run when it goes out of scope, preventing a double free.

In essence, a move in Rust for types that manage heap resources avoids expensive deep copies by simply copying the small, fixed-size ‘handle’ or ‘metadata’ and transferring the unique ownership rights to the underlying resource.

A Note on Function Calls and Borrowing (e.g., println!)

You might wonder why passing a String to the println! macro doesn’t move ownership, allowing you to use the String afterwards:

fn main() {
    let message = String::from("Hello, Rust!");
    println!("First print: {}", message); // Pass owned String to println!
    println!("Second print: {}", message); // Still valid, can use message again!
}

This works because println! is a macro. Macros can be more flexible than regular functions. println! expands into code that uses formatting traits, and these traits typically operate on references. When you pass an owned String, the macro expansion effectively takes an immutable reference (&String, which often further dereferences to &str for formatting) for the duration of the call. It borrows the value rather than consuming it, leaving the original message variable and its ownership intact. While some generic functions can also accept different types via traits that involve borrowing (like AsRef), the specific ability of println! to seem like it takes ownership but doesn’t is characteristic of its macro implementation. Contrast this with regular functions taking String by value, which do move ownership as shown previously.

Comparison with C++ and C

  • C++: Assignment (std::string s2 = s1;) typically performs a deep copy. To achieve move semantics, you must explicitly use std::move: std::string s2 = std::move(s1);. After moving, s1 is left in a valid but unspecified state. Passing by value also typically copies unless std::move is used or specific compiler optimizations occur (like RVO/NRVO for returned values).
  • C: Assigning pointers (char *s2 = s1; where s1 is malloced) creates a shallow copy—both pointers refer to the same memory. Passing pointers copies the pointer value, still resulting in shared mutable state without ownership tracking. There’s no compile-time help to prevent double frees or use-after-free if one pointer is used after the memory has been freed via the other pointer.

Rust’s default move semantics enforce single ownership, preventing these C/C++ issues at compile time.

6.2.2 Simple Value Copies: The Copy Trait

Types whose values can be duplicated via a simple bitwise copy implement the Copy trait. This applies to types with a fixed size known at compile time that do not require special cleanup logic (i.e., they don’t implement Drop). When assigned or passed by value (either to another variable or as a function argument), variables of Copy types are duplicated (copied), and the original variable remains valid and usable. Examples include integers, floats, booleans, characters, and tuples/arrays containing only Copy types.

fn makes_copy(some_integer: i32) { // some_integer gets a copy
    println!("Inside function: {}", some_integer);
} // some_integer (the copy) goes out of scope.

fn main() {
    let x = 5;    // i32 implements Copy
    let y = x;    // y gets a COPY of x's value. x is still valid.
    println!("x: {}, y: {}", x, y); // Both usable. Prints: x: 5, y: 5
    makes_copy(x); // x is copied into the function.
    println!("x after function call: {}", x); // x is still valid and usable here.
}

These types are Copy because copying their bits is cheap and sufficient to create a new, independent value. There’s no owned resource (like a heap pointer) requiring unique ownership or cleanup via Drop. Types implementing Drop cannot be Copy, as implicit copying would make resource management ambiguous.

6.2.3 Explicit Deep Copies: The Clone Trait

If you need a true duplicate of data managed by an owning type (like String or Vec<T>) – meaning, new heap allocation and copying the data – you must explicitly request it using the .clone() method. This requires the type to implement the Clone trait (most standard library owning types do).

fn main() {
    let s1 = String::from("duplicate me");
    let s2 = s1.clone(); // Explicitly performs a deep copy. s1 remains valid.
    println!("s1: {}, s2: {}", s1, s2); // Both are valid and own independent data.
} // s1 is dropped, then s2 is dropped. Each frees its own memory.

Because cloning can be expensive (memory allocation and data copying), Rust makes it explicit via a method call. This encourages programmers to consider whether they really need a full copy or if borrowing (using references, discussed next) would be more efficient. Note that for Copy types, clone() is usually implemented as just a simple copy.