DEV Community

Brian Maher
Brian Maher

Posted on • Edited on

Rust Borrow Checker Explained Part 1: Copying & Moving

Intro

One way to avoid borrowing completely is to make copies, which is the focus of this article:

  • How memory is managed in Rust using the "stack".
  • How the Copy marker trait is used to denote "trivial" types that are implicitly copied.
  • How the Clone trait allows you to avoid borrowing completely, but at the expense if copying data around.
  • "Moving" rules.

Stack Memory Management

All memory is managed by a stack per thread in Rust. When calling functions, the arguments are copied onto the stack. During execution of the function, any local variables are pushed onto the stack, too. Finally, when the function returns, any local variables and function arguments are popped off the stack and replaced with the return value(s) of the function.

fn main() {
    let num = 2;
    let even = is_even(num);
    print!("is_even={even} num={num}")
}

fn is_even(num: i32) -> bool {
    num % 2 == 0
}
Enter fullscreen mode Exit fullscreen mode

Try it!

In this simple example, main is called with no arguments, so the stack is empty. Technically this isn't entirely true, since a "program counter" is always recorded into the stack on every function call so the code knows where to resume execution on return, but we will gloss over that detail for now.

The first line of code in main pushes a local variable num onto this stack and assigns it the value 2. So, now we have a stack of one value: [2].

TODO: Display a stack visual

The second line of code copies num onto the stack and calls is_even. So, now our stack has two values: [2, 2].

TODO: Display a stack visual

The is_even function checks if the top of the stack is an even value, and then returns this fact. So, at the end of this function, the stack is now [2, true] (the 2 function argument gets popped and the function result true is pushed).

TODO: Display a stack visual

The result of the function is stored in a local variable even, and so the stack continues to contain [2, true] after the second line completes.

TODO: Display a stack visual

The final line of code prints both the local variables stored on the stack.

When main returns, the two local variables are popped off the stack.

TODO: Display a stack visual

The Copy trait

In our first example, we worked with the i32 type. Let's replace this with our own type the wraps the i32 and see what happens:

#[derive(Debug)]
struct Num(i32);

fn main() {
    let num = Num(2);
    let even = is_even(num);
    print!("is_even={even:?} num={num:?}")
}

fn is_even(num: Num) -> bool {
    num.0 % 2 == 0
}
Enter fullscreen mode Exit fullscreen mode

Try it!

The #[derive(Debug)] and extra :?s in the print! statement are to avoid implementing the Display trait, you can ignore that for now.

Overall, this is the same code as our first example, but the i32 is wrapped into a Num struct... but the compiler gives us this error:

error[E0382]: borrow of moved value: `num`
  --> src/main.rs:7:34
Enter fullscreen mode Exit fullscreen mode

Note that this isn't the entire error message, since the Rust compiler tries very hard to give you more useful information on how to fix the problem.

So, what is different about the Num struct vs the i32? The difference is that i32 "implements" the Copy trait. So, let's change Num so it implements the Copy trait and see if it compiles:

// Notice the "Clone" is also derived
#[derive(Debug, Clone)]
struct Num(i32);

// Notice the "Copy" implementation:
impl Copy for Num {}

fn main() {
    let num = Num(2);
    let even = is_even(num);
    print!("is_even={even:?} num={num:?}")
}

fn is_even(num: Num) -> bool {
    num.0 % 2 == 0
}
Enter fullscreen mode Exit fullscreen mode

Try it!

The Copy trait is a "marker" trait and thus it has no implementation body. In other languages this pattern of implementing a trait which has no code associated with it, might be called a "marker interface" or "tagging interface".

Therefore, the impl Copy for Num {} is all that is needed to implement this trait. Note that this trait requires the Clone trait to be implemented, so I added that to the #[derive(Debug, Clone)].

Another way to implement both Clone and Copy is to derive this using: #[derive(Debug, Copy)].

Let's make one small change to the Num struct so it has an English string that describes the number:

// Notice the extra `String` field:
#[derive(Debug, Clone)]
struct Num(i32, String);

impl Copy for Num {}

fn main() {
    let num = Num(2, "two".into());
    let even = is_even(num);
    print!("is_even={even:?} num={num:?}")
}

fn is_even(num: Num) -> bool {
    num.0 % 2 == 0
}
Enter fullscreen mode Exit fullscreen mode

Try it!

Now we get this error:

error[E0204]: the trait `Copy` cannot be implemented for this type
 --> src/main.rs:4:15
Enter fullscreen mode Exit fullscreen mode

This is because all fields of the struct must implement Copy, but the String type does not implement this.

Although it would be handy if String implemented Copy, it is also a potentially computationally expensive operation, since a String is not a fixed size. Imagine if you had a million character string that you passed around. Every time it was passed into a function, your program would need to copy the million characters.

However, for simple types (like enums with no associated values), it is highly recommended that you just #[derive(Copy)].

Cloning

One way to solve the problem of structures that can't implement Copy is to explicitly clone() them.

// Notice the Clone is derived and NOT Copy
#[derive(Debug, Clone)]
struct Num(i32, String);

fn main() {
    let num = Num(2, "two".into());
    // Notice the call to `.clone()`
    let even = is_even(num.clone());
    print!("is_even={even:?} num={num:?}")
}

fn is_even(num: Num) -> bool {
    num.0 % 2 == 0
}
Enter fullscreen mode Exit fullscreen mode

Try it!

Although this is a great "hammer" to have in your toolbox, you do need to be aware of the performance implications in case the thing being copied around is large.

Cloning is also not great for any containers that are managing resources. For example, you probably shouldn't be clone()ing a file descriptor or a mutex.

To manage resources, it is recommended to implement the Drop trait so that resources can automatically be "closed" just before their memory is released. The Drop trait is analogous to implementing Java's AutoCloseable interface, and using a try-with-resource block to ensure the resource is "closed", or Golang's defer statement.

Moving (aka shallow copy)

All datatypes in Rust can be "moved", which is just a fancy name for taking a shallow bit-for-bit copy of a datatype. Moving does come with a big restriction though: you can't continue to use the value, and thus in this final example, the is_even() function was modified so it returns the Num that was passed into it.

#[derive(Debug)]
struct Num(i32, String);

fn main() {
    let num = Num(2, "two".into());
    // num is "moved" in here:
    let (even, num) = is_even(num);
    // ...which means we can NOT print it any more
    print!("is_even={even:?} num={num:?}")
}

fn is_even(num: Num) -> (bool, Num) {
    // num is "moved" out here:
    (num.0 % 2 == 0, num)
}
Enter fullscreen mode Exit fullscreen mode

Try it!

If you try to remove the re-assignment to num:

#[derive(Debug)]
struct Num(i32, String);

fn main() {
    let num = Num(2, "two".into());
    // notice that we no longer re-assign to num!
    let (even, _) = is_even(num);
    // ...which means we can NOT print it any more
    print!("is_even={even:?} num={num:?}")
}

fn is_even(num: Num) -> (bool, Num) {
    // num is "moved" out here:
    (num.0 % 2 == 0, num)
}
Enter fullscreen mode Exit fullscreen mode

You will get this error:

error[E0382]: borrow of moved value: `num`
  --> src/main.rs:9:34
Enter fullscreen mode Exit fullscreen mode

This is because line 7 is "stealing" the num, and thus all subsequent attempts to use num (like when it performs "print") will fail.

Rust enforces this constraint, since a shallow copy could be stored in a global variable, but there is now multiple shallow copies that are potentially sharing data, since the "deep" parts of that data did not get copied. How would the borrow checker know when the "deep" parts of the data are no longer used?

Recursive Data Structures: Box

In order for the compiler to know how much space is needed to store a structure on the stack, it needs to know the exact size of that structure. Therefore if you try to define a recursive data structure like this:

#[derive(Debug)]
struct Num(i32, Option<Num>);

fn main() {
    let num = Num(2, None);
    print!("num={num:?}")
}
Enter fullscreen mode Exit fullscreen mode

Try it!

You will get this error:

error[E0072]: recursive type `Num` has infinite size
 --> src/main.rs:2:1
Enter fullscreen mode Exit fullscreen mode

To get around this limitation, you instead need to have the memory stored on the "heap", and instead just have a unique pointer stored on the stack. You can use the Box datatype in Rust to do this.

Let's try to modify the recursive data structure by using a Box:

#[derive(Debug)]
struct Num(i32, Option<Box<Num>>);

fn main() {
    let num = Num(2, None);
    print!("num={num:?}")
}
Enter fullscreen mode Exit fullscreen mode

Try it!

Note that all the same ownership constraints are the same for a Box. However, a Boxs size doesn't need to be known at compile time since it dynamically allocates memory on the heap. Recursive data structures are just one example of where this is needed, but this is also needed when working with dyn traits, but this series won't get into the details of that.

Copy & Clone Recap

To recap:

  • Simple types (in Java these are called "primitives") should implement Copy in order to get implicit copy semantics.
  • Other types, should implement Clone, and you can use the .clone() "hammer" to avoid borrowing. Notice how using this "hammer" is very explicit in our code, since it requires calling .clone().
  • Any non-memory resource should not implement Clone and should instead implement the Drop trait to have the resource automatically released.
  • Rust allows "moving" values to make shallow clones, and if you use this, you will need to return the "moved" value if you want to subsequently make use of it.

To learn how to actually borrow values, please go to Part 2

Top comments (1)

Collapse
 
louys profile image
TQLeung

Thanks and I hope to see UNSAFE tutorial