Rust Mutability, Moving and Borrowing - The Straight Dope


#1

Over the past few months I’ve been using Rust A LOT and I wanted to post a casual article explaining some of the oddities of Rust that I struggled with coming from C as a way to give back to the community and help others. Huge thanks to everyone on this board for helping me in understanding these concepts!

Who should read this article?

You should read this article if:

  1. You are new to Rust and struggling understanding the “move semantics” and borrow system
  2. You find yourself practically ripping your hair out over compiler errors concerning mutability, borrows, and moves
  3. You are already a programmer or know at least one other language well, especially a low-level/native language

I highly discourage you from reading this article if you are not already a programmer or are new to computer programming. Rust is a difficult language to learn and I honestly cannot recommend it as a first language at this point in time due to limited learning resources and complex subjects which are learned much easier with in-depth knowledge of how computer memory works. If you choose to continue anyway, don’t let Rust discourage you from an awesome learning experience in computer programming.

Introduction

I will openly state that learning Rust was the most steep learning curve for me than any other programming language, ever. C++ would be a close second, but that is out of x86-64 assembly, C, C#, Python, PHP, JavaScript, CSS (although I still hate CSS), SQL, and probably others that I forgot to mention.

But why? What makes Rust so difficult to learn?

Well, in other languages, there are conventions… Sure, JavaScript and C are very different, Python and C# are very different, but in each of those languages, it is pretty standard to be able to pass data around freely between functions without having to reason about when the data is freed, if it’s freed, accidentally referencing uninitialized data, sending the program into corrupted memory, etc…

However, most of those languages also feature a runtime environment which runs alongside your program and keeps it in check via a garbage collector and exception handling. This is all fine and good except there is a large runtime performance cost, the system’s complexity is increased, and the programmer loses some low-level control over the program’s memory.

On the other hand, lower-level languages such as C and C++ just let you shoot yourself in the foot - meaning, they won’t stop you from writing code with a ton of memory errors in it - but your program will suddenly crash with a not-so-descriptive error message. This leads to programmers spending hours and hours of time in a debugging stepping through the code trying to figure out what went wrong… And if it’s not at source-level, then may even have to single-step through assembly instructions, such as when a compiler update modifies the behavior of the output all things left the same in the source code.

Rust takes a much different approach - in exchange for a steeper learning curve, going to hell and back at war with the compiler at first, and in some cases, slightly more verbose code, the programmer gets a 100% guarantee that his/her code is completely free of ANY AND ALL memory bugs. Memory bugs include buffer overflows, use-after-free, derefencing unitialized data, null ptr dereference, double free, and many other memory errors which would ordinarily lead to security vulnerabilities, crashed programs, and unhappy users. In fact, a great deal of money is spent on so-called static analysis tools as a response to the aforementioned problems, which are completely prevented in safe Rust.

Note, I said “Safe Rust.” Rust does have the option to use unsafe blocks, and what you do in there, there are no memory safety guarantees. The good news is, you rarely should need to use unsafe blocks and as the language matures, it will likely mean that unsafe code will be necessary less and less.

To Hell and Back with the Compiler - Moving and Borrowing

Now, let’s learn you some Rust. If you’ve already tried writing some Rust and building it, I promise you that you’ve seen errors about mutability, moving, and borrowing. It is inevitable… In fact, I wouldn’t be surprised if you’ve seen these errors thousands of times if you are new to the language… And began to question why you are even using Rust, why you bothered trying to learn it, and perhaps even questioning your sanity.

For this reason, I’m going to take an absolute no-nonsense, start-from-scratch approach at explaining moving, borrowing, the differences between them, the errors you will see, what they mean, and how to handle all of it. Yes, there is the Rust Book, and there are a few other resources online here and there, but in this post, I’m going to really try and make this “idiot-proof” and spare some of the super technical jargon in favor of helping the reader understand all of this stuff to begin with. That being said, this article is on the usage of Rust as a programmer, not the implementation of the language. I could get into the technical details of how the language is actually implemented and what is going on behind the scenes but that would do far more harm than good and is a task for a much later time when you know moving and borrowing like the back of your hand first.

TLDR - Agenda:

  1. You will learn Moving and Borrowing
  2. You will not learn about lifetime specifiers - that will be the next article
  3. You will be looking at and working with a lot of code- all examples will have runnable links provided. In fact, you don’t even need Rust installed!
  4. We are using an informal voice in favor of understanding

Mutability

First and foremost, the most common error a new Rust programmer will probably discover is that the following code will generate error:

error[E0384]: cannot assign twice to immutable variable name


fn main()
{
    let name = "Thomas";
    
    name = "Jane";
    
    println!("Name is: {}", name);
}

Try me

In Rust, all variables which are assigned using a let statement are immutable unless otherwise specified. Immutable is a fancy term which means:

You cannot change my value once I have been initially set.

Since we’ve tried to change the immutable variable name to Jane after it was set to Thomas, we receive that error.

To fix this error, simple add mut before name = "Thomas"; like so:


fn main()
{
    let mut name = "Thomas";
    
    name = "Jane";
    
    println!("Name is: {}", name);
}

References

Just like in C or C++, we can create a reference or pointer to data… Meaning, we can store the address of where a variable is in another variable. Observe:

fn main()
{
    let mut name = "Thomas";
    
    name = "Jane";
    
    let name_ref = &name;
    
    println!("Name is: {}", name);
}

Here, name_ref is an immutable reference to name.

WHOA what??? I thought name was mutable now???

CONFUSION ALERT - The reference type is completely independant of the original variable’s data type… This means we can create an immutable reference to a mutable variable, (shown above). However, we cannot create a mutable reference to an immutable variable. Meaning, while the above is totally legal, the below will generate error:

error[E0596]: cannot borrow immutable local variable name as mutable

fn main()
{
    let name = "Thomas"; 
    let name_ref = &mut name;
    
}

Try me

Why not?

If you look closely, you’ll notice that creating a mutable reference to immutable data would make no sense - we cannot mutate the data, so why would we need a mutable reference?

On the other hand, creating an immutable reference to mutable data makes perfect sense - maybe we want a function to read data but not be able to change it… Perfectly legal and has many use-cases.

In summary: We can create immutable references to mutable variables as well as immutable variables, but we can only create mutable references to mutable variables.

Now, let’s introduce a complex type that we will use for the remainder of the article in demonstrations:

Player struct

This article is meant to show real-world examples of code, not just as basic crap as possible, hence the recommendation of already being a programmer for the intended audience.


struct Player
{
    name: String,
    age: u8,
    description: String
}

Here we have a complex type called “struct Player” which has two Strings and a unsigned 8 bit integer. Pretty basic.

Now, we’re going to new up a Player object like so:


    let mut our_player = Player
    {
        name: "Jones".to_string(), //converts &str (static string) to a String (heap memory)
        age: 25,
        description: "Just a happy guy.".to_string()
    };

What the hell is a “move?”

In Rust, a move means that data’s ownership has been handed over from one function or method to another. It’s very similar to automobile ownership. With cars, the owner holds the car and the title. Once he transfers the car and title to someone else, he no longer owns the car and he does not have access or control over it. Now, let’s look at some code:

struct Player
{
    name: String,
    age: u8,
    description: String
}

fn main()
{
let mut our_player = Player
    {
        name: "Jones".to_string(),
        age: 25,
        description: "Just a happy guy.".to_string()
    };
    
    mover(our_player);
    
    println!("My name is {}, and I am being used after a move", our_player.name);
    
}

fn mover(moved: Player) -> Player
{
    println!("I am {}, I've been moved into mover", moved.name);
    moved
}

Try Me

The above code generates this compiler error:

error[E0382]: use of moved value: our_player.name

WHY???

This error is generated due to two lines of code:

Line 1: fn mover(moved: Player) -> Player, the function signature.

Line 2: mover(our_player);, the function call

Specifically, since we specify that mover() takes a Player and not a &Player (immutable reference to Player), Rust transfers ownership of our_player to mover() permanently. This means that main() no longer has any control over or access to our_player! WHOA… Much different than other programming languages where our_player would simply be “passed by value” here.

To fix this, we change the function signature to accept a &Player and we pass in &our_player instead of our_player. Make sense??? See below:

struct Player
{
    name: String,
    age: u8,
    description: String
}

fn main()
{
let mut our_player = Player
    {
        name: "Jones".to_string(),
        age: 25,
        description: "Just a happy guy.".to_string()
    };
    
    immutable_borrow(&our_player);
    
    println!("My name is {}, and I am being used after an immutable borrow", our_player.name);
    
}

fn immutable_borrow(borrowed: &Player)
{
    println!("I am {}, I've been immutably borrowed", borrowed.name);
}

Try Me

Notice the delta here - I implemented what I described before the last code block but in order to not confuse the reader, I’ve changed the function name from mover() to immutably_borrow and changed the text inside of the println! to reflect that.

BUT WAIT - YOU WILL PULL YOUR HAIR OUT OVER THIS!!!

So, check out this code, which happens to compile just fine:

fn main()
{
    let age: u8 = 55;
    mover(age);
    
    println!("The age is: {}", age);
}

fn mover(age: u8) -> u8
{
    println!("Age {} has been moved into mover!", age);
    age
}

Try me

WHAT THE &$(@&#(@#&???

Didn’t I just show that when data is passed into a function without using a & reference, it is moved and thus ownership has been transferred out of main() (or some other calling function) and cannot be accessed? Thus, println!() should not work because main() no longer owns age, right?

WRONG!!!

So it turns out, Rust has a nasty little secret that is not super obvious upfront… It has a Trait called Copy and any data type which implements that Trait, the Rust compiler does not move. Instead, it copies the data into another function or variable, much like the typical “pass by value” behavior in other languages. There’s no syntactic way to tell this at first. However, if you were to study the Rust documentation, you could eventually figure this out… But at first, it just looks like total inconsistency on the compiler’s part because with some types, such as our Player type, it would move the data and generate the error, while other types like the u8, u16, u32, and all other “primitive types,” it would copy and compile just fine!

So the trick here is before you get annoyed about why or why not you are getting compiler errors about moves, check and see if the data type you are trying to move implements the Copy trait. Player does not and neither does the standard String type.

There is no mutable/immutable move

A move is simply a move, that’s it. Once data has been moved, it is completely owned by whomever it is moved to and the owner is free to mutate, read, or destroy it (when the current scope/block ends). Rust automatically destroys data for you, so you won’t be seeing any deletes.

What on earth is a “borrow?”

Unlike a move, a borrow, as we already introduced in a code example above, is when you pass a reference to data rather than the data itself using the &. However, there are two types of borrows and we’ve only shown one thus far:

Immutable Borrows

This is what we demonstrated above. Just to review:

    immutable_borrow(&our_player);
    
    println!("My name is {}, and I am being used after an immutable borrow", our_player.name);
    
}

fn immutable_borrow(borrowed: &Player)
{
    println!("I am {}, I've been immutably borrowed", borrowed.name);
}

That code illustrates an immutable borrow… You can pass around as many of these as you like, as long as there are no mutable borrows in scope. So what is a mutable borrow and how is it different? An immutable borrow allows whomever is borrowing it to read only and not modify the data. A mutable borrow allows whomever is borrowing it to actually modify (mutate) the data. As you may guess, mutable borrows introduce much more complexity and are policed by the compiler much more than immutable borrows are.

Mutable Borrows

Look at the following code:

struct Player
{
    name: String,
    age: u8,
    description: String
}

fn main()
{
let mut our_player = Player
    {
        name: "Jones".to_string(),
        age: 25,
        description: "Just a happy guy.".to_string()
    };
    
    immutable_borrow(&our_player);
    change_name(&mut our_player);
    println!("My name is {}, and I am being used after an mutable borrow", our_player.name);
    
}

fn immutable_borrow(borrowed: &Player)
{
    println!("I am {}, I've been immutably borrowed", borrowed.name);
}

fn change_name(borrowed: &mut Player)
{
    borrowed.name = "My New Name".to_string();
}

Try me

I want you to pay close attention to the syntactic differences between an immutable borrow and a mutable one. Here, our mutable borrow occurs in the function change_name() which does as it says - changes the name of our_player. First, let’s look at the function signature: fn change_name(borrowed: &mut Player)

Notice, instead of Player, or &Player, we use &mut Player. This tells the Rust compiler that we are preparing to accept a mutable borrow.

Also notice that we must also specify it in every single call explicitly: change_name(&mut our_player);

We cannot just call change_name(our_player) and we also cannot call change_name(&our_player). We must call change_name(&mut our_player).

This is also a big gotcha for newbies, they will specify the proper function signature, but forget the &mut when they call the function.

Now, let’s talk about semantics. How is a mutable borrow treated differently than an immutable borrow? Well, as you may imagine, ** (safe) Rust only allows one mutable reference to data in scope at any given time, period, no questions asked.** I say safe Rust because this can be overriden by using unsafe blocks which we are not covering here and should be avoided if at all possible.

**Also note that Rust will not allow ANY immutable references to data when there is an active mutable reference! **This is to prevent data races which occur when one piece of code is trying to read the data while another is writing to it - these introduce nasty bugs.

Let’s view an example of code which will not compile:

struct Player
{
    name: String,
    age: u8,
    description: String
}

fn main()
{
let mut our_player = Player
    {
        name: "Jones".to_string(),
        age: 25,
        description: "Just a happy guy.".to_string()
    };
    
    let my_immutable_return = immutable_borrow(&our_player);
    change_name(&mut our_player);
    println!("My name is {}, and I am being used after an mutable borrow", our_player.name);
    
}

fn immutable_borrow(borrowed: &Player) -> &Player
{
    println!("I am {}, I've been immutably borrowed", borrowed.name);
    borrowed
}

fn change_name(borrowed: &mut Player)
{
    borrowed.name = "My New Name".to_string();
}

Try me

Here, we’ve immutably borrowed our_player into my_immutable_return. We’ve then tried to mutably borrow our_player while it is stilled immutably borrowed.

Wait, what???

Yes, so when we create a borrow like this:
let my_immutable_return = immutable_borrow(&our_player);
It does not get “returned to its owner” until the curly brace after it. The problem here is that my_immutable_return has borrowed my_player until the curly brace at the end of the main() function. However, before we’ve reached the end of main(), we’ve attempted to mutably borrow our_player. We get this error:

error[E0502]: cannot borrow our_player as mutable because it is also borrowed as immutable

All borrows in Rust end at the end of the “code block” aka the curly brace following the borrow.

This means that the following code compiles and runs just fine:

struct Player
{
    name: String,
    age: u8,
    description: String
}

fn main()
{
let mut our_player = Player
    {
        name: "Jones".to_string(),
        age: 25,
        description: "Just a happy guy.".to_string()
    };
    {
        let my_immutable_return = immutable_borrow(&our_player);
    }
    change_name(&mut our_player);
    println!("My name is {}, and I am being used after an mutable borrow", our_player.name);
    
}

fn immutable_borrow(borrowed: &Player) -> &Player
{
    println!("I am {}, I've been immutably borrowed", borrowed.name);
    borrowed
}

fn change_name(borrowed: &mut Player)
{
    borrowed.name = "My New Name".to_string();
}

Try me

The only change I made is that I added curly braces around let my_immutable_return = immutable_borrow(&our_player); which caused the borrow to end on line 18 of the Rust playground (click Try me above). Because the borrow ended earlier, now, we are free to mutably borrow our_player again.

Moving and Borrowing summary

One critical error that many new Rust programmers can make coming from languages like C and C++ is that we see & and we think “pass by reference” and when we see no & we think “pass by value.” This will cause you to freak out a lot… I’ve found that the best way to think as a Rustacean is instead to see & and think “pass by borrow” and when we use no & we are “passing by move.”

Whew, that was a lot… Please do experiment yourself in the Playground examples… Create more compiler errors, see what happens when you try to borrow twice and three times, try to move twice, etc… It takes some getting used to.

Other common pitfalls that will destroy you

I want to show you something else that has to do with Structs in Rust which are similar (but not the same) as classes in other languages. Rust does have methods. See below:

struct Player
{
    name: String,
    age: u8,
    description: String
}

impl Player // This just means "define methods on Player"
{
    fn print_me(self)
    {
        println!("Name: {}\nAge: {}\nDescription:{}", self.name, self.age, self.description);
    }
}

fn main()
{
    let mut our_player = Player
    {
        name: "Jones".to_string(),
        age: 25,
        description: "Just a happy guy.".to_string()
    };
    
    our_player.print_me();
}

This code runs just fine… In fact, it’s pretty easy to understand and very Python-like, huh?

But let’s call our_player.print_me() again:

struct Player
{
    name: String,
    age: u8,
    description: String
}

impl Player // This just means "define methods on Player"
{
    fn print_me(self)
    {
        println!("Name: {}\nAge: {}\nDescription:{}", self.name, self.age, self.description);
    }
}

fn main()
{
    let mut our_player = Player
    {
        name: "Jones".to_string(),
        age: 25,
        description: "Just a happy guy.".to_string()
    };
    
    our_player.print_me();
    our_player.print_me();
}

Try me

BANG!!! Doesn’t compile!!!:

error[E0382]: use of moved value: `our_player`
  --> src/main.rs:26:5
   |
25 |     our_player.print_me();
   |     ---------- value moved here
26 |     our_player.print_me();
   |     ^^^^^^^^^^ value used here after move

What on earth???

So, in Rust, turns out that the way we’ve defined our print_me() method, we’ve specified self. Seems legit, right???

WRONG!

What’s actually happening here is a bit perplexing at first, but we’ve moved the our_player object into its own print_me() method, and then discarded it on the first call to our_player.print_me() such that when we try to call it again, print_me() cannot access its own parent object!

Luckily, although this is a nasty error that can be tough to understand, it has a very simple fix… We change the method signature from fn print_me(self) to fn print_me(&self).

That’s right - one single & fixes this problem… Now, we’ve immutably borrowed the parent object so that it is not permanently moved into its own print_me() method.

I’d like to also point out that here print_me() does nothing other than read the data inside of our_player but if it were to change the data, we would get another error again because we’ve only passed an immutable reference to the function. To resolve this, we would need to change the signature to fn print_me(&mut self). I hope this is starting to all make sense now… If we need to modify the data, we need a mutable reference to it, even in the case of struct objects. Let’s look at one more example to solidify this:

struct Player
{
    name: String,
    age: u8,
    description: String
}

impl Player // This just means "define methods on Player"
{
    fn print_me(&self)
    {
        println!("Name: {}\nAge: {}\nDescription:{}", self.name, self.age, self.description);
    }
    
    fn change_me(&mut self)
    {
        self.name = "changed".to_string();
        self.age = 240;
    }
}

fn main()
{
    let mut our_player = Player
    {
        name: "Jones".to_string(),
        age: 25,
        description: "Just a happy guy.".to_string()
    };
    
    our_player.print_me();
    our_player.change_me();
    our_player.print_me();
}

And finally we get the output:

Name: Jones
Age: 25
Description:Just a happy guy.
Name: changed
Age: 240
Description:Just a happy guy.

Try me

Bonus Trait Jargon - Is it Send???

I’d like to point out one last thing - you will often see Rustaceans saying things like:

Is Player Display?

Is u32 Copy?

MyCoolType is Display, Copy, and Clone

This is shorthand for:

Does the Player type implement the Display trait?

Does the u32 type implement the Copy trait?

MyCoolType implements the Display, Copy, and Clone traits

That’s all folks - I hope this has really helped clear up some confusion of the Rust move semantics and borrow system. May update this post, and experienced Rustaceans feel free to drop feedback.


#2

Awesome, thanks for writing this up - helped me a ton!


#3

@dakom no prob, glad it was helpful.

EVERYONE - I’ve released a video tutorial to go along with this article here for you folks who prefer videos:


#4

I’d like to add that lifetimes happen to be where the learning curve starts getting real steep (Everything you know about lifetimes is wrong). And also suggest that borrows shouldn’t really be compared to pointers in C or references in C++. Borrows fit the model of read locks (shared) and write locks (exclusive), and that analogy might be a better teaching tool.


#5

Well stated.

I suspect that the “pointer/reference” analogy arises in part from the apparently complimentary actions of the & and * prefix operators, and in part from rustc’s tendency to materialize pointers within unoptimized compiles. Compiler optimization or future improvements will often remove these “pointers/references”. Thus, if OP or others feel the need to refer to borrows in such terms, perhaps they should call them “virtual pointers/references”.


#6

Absolutely. I always thought of borrows as pointers for this reason. But when I learned that Rust has raw pointers, I was able to give up that notion with barrows. And I’ve only used raw pointers with embedded systems for accessing memory mapped registers. It made the distinction really quite clear.


#7

Given this - it would be nice to see more explanation of & vs. * as compared to the C counterparts.

From looking at some code here and there - I see some &*var type stuff and it’s hard to understand what this means, at a glance.


#8

That’s called a reborrow. &*foo can be used to transform a &mut T to &T, for example. Rust ownership the hard way describes reborrowing in the more common case of transforming &mut T to a new &mut T to avoid mutable aliasing.


#9

Excellent - and the article is a great overview!


#10

Dope AF! I commend your effort to expand on the hard parts of Rust!