Understanding Move Semantics, Copy, and Clone in Rust

In Rust, when you assign a value to a new variable or pass it to a function, the behavior depends on whether the type implements the Copy trait. Let's explore this with some examples.

Move Semantics

By default, if a type doesn't implement the Copy trait, assigning it to a new variable or passing it to a function will move the value instead of copying it.

Here's an example with String, which doesn't implement Copy:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;  // s1 is moved to s2

    println!("{}", s1);  // This line would cause a compile-time error
}

This code won't compile. You'll get an error like this:

error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:20
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;  // s1 is moved to s2
  |              -- value moved here
4 | 
5 |     println!("{}", s1);  // This line would cause a compile-time error
  |                    ^^ value borrowed here after move

The ownership of the string data has moved from s1 to s2, and s1 is no longer valid to use.

The Copy Trait

Types that implement the Copy trait behave differently. When you assign a Copy type, it's copied instead of moved. Most primitive types (like integers, floats, and booleans) implement Copy.

Here's an example:

fn main() {
    let x = 5;
    let y = x;  // x is copied to y

    println!("x = {}, y = {}", x, y);  // This is fine, x is still valid
}

This code works because i32 (the type of x) implements Copy. Both x and y are usable after the assignment.

The Clone Trait

For types that don't implement Copy (like String), Rust provides the Clone trait. Clone allows you to explicitly create a deep copy of a value.

Here's how you can use Clone with String:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();  // Explicitly clone s1

    println!("s1 = {}, s2 = {}", s1, s2);  // This is fine, s1 is still valid
}

In this case, s2 gets a new allocation with a copy of the data from s1. Both s1 and s2 are valid and independent.

Implementing Copy and Clone

You can implement Copy and Clone for your own types. Here's an example:

#[derive(Debug, Copy, Clone)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p1 = Point { x: 5, y: 10 };
    let p2 = p1;  // p1 is copied to p2 because Point implements Copy

    println!("p1 = {:?}", p1);  // This is fine, p1 is still valid
    println!("p2 = {:?}", p2);
}

In this example, Point implements both Copy and Clone. When we assign p1 to p2, it's copied, not moved.

Key Points to Remember

  1. By default, assigning non-Copy types moves the value.
  2. Types that implement Copy are automatically copied when assigned or passed to functions.
  3. Clone allows explicit copying of values, even for types that don't implement Copy.
  4. Copy is implicit and cheap, while Clone is explicit and may be more expensive.
  5. Not all types can implement Copy. For example, types that manage resources (like String or Vec) typically don't implement Copy.

Understanding these concepts is crucial for writing efficient Rust code and avoiding unexpected behavior related to ownership and borrowing.

Subscribe to maventype

Sign up now to get access to the library of members-only issues.
Jamie Larson
Subscribe