6.3 Borrowing and References

In Rust, borrowing grants access to a value without transferring ownership. This is done with references, which come in two forms: immutable (&T) and mutable (&mut T). While references in Rust resemble raw pointers in C, they are subject to strict safety guarantees preventing common memory errors. In contrast, C pointers can be arbitrarily manipulated, sometimes leading to undefined behavior. Because Rust checks references thoroughly, they are often called managed pointers.

6.3.1 References in Rust vs. Pointers in C

Rust References

  • Immutable (&T): Read-only access.
  • Mutable (&mut T): Read-write access.
  • Non-nullable: Cannot be null.
  • Always valid: Must point to valid data.
  • Automatic dereferencing: Typically do not require explicit * to read values.

C Pointers

  • Nullable: May be null.
  • Explicit dereferencing: Must use *ptr to access pointed data.
  • No enforced mutability rules: C does not distinguish between mutable and immutable pointers.
  • Can be invalid: Nothing stops a pointer from referring to freed memory.

Example

fn main() {
    let x = 10;
    let y = &x; // Immutable reference
    println!("y points to {}", y);
}
#include <stdio.h>

int main() {
    int x = 10;
    int *y = &x; // Pointer to x
    printf("y points to %d\n", *y);
    return 0;
}

6.3.2 Borrowing Rules

Rust’s borrowing rules are:

  1. You can have either one mutable reference or any number of immutable references at the same time.
  2. References must always be valid (no dangling pointers).

Immutable References

Multiple immutable references are permitted, whether or not the underlying variable is mut:

fn main() {
    let s1 = String::from("hello");
    let r1 = &s1;
    let r2 = &s1;
    println!("{}, {}", r1, r2);

    let mut s2 = String::from("hello");
    let r3 = &s2;
    let r4 = &s2;
    println!("{}, {}", r3, r4);
}

Having multiple references to the same data is sometimes called aliasing.

Single Mutable Reference

Only one mutable reference is allowed at any time:

fn main() {
    let mut s = String::from("hello");
    let r = &mut s; // Mutable reference
    r.push_str(" world");
    println!("{}", r);
}

Why Only One?
This rule ensures no other references can read or write the same data concurrently, preventing data races even in single-threaded code.

Note that you can only create a mutable reference if the data is declared mut. The following code will not compile:

fn main() {
    let s = String::from("hello");
    let r = &mut s; // Error: s is not mutable
}

In the same way, an immutable variable cannot be passed to a function that requires a mutable reference.

Invalid Code: Mixing a Mutable Reference and Owner Usage

fn main() {
    let mut s = String::from("hello");
    let r = &mut s;
    r.push_str(" world");

    s.push_str(" all"); // Error: s is still mutably borrowed by r
    println!("{}", r);
}

Here, s remains mutably borrowed by r until r goes out of scope, so direct usage of s is forbidden during that time.

Possible Fixes:

  1. Restrict the mutable reference’s scope:

    fn main() {
        let mut s = String::from("hello");
        {
            let r = &mut s;
            r.push_str(" world");
            println!("{}", r);
        } // r goes out of scope here
    
        s.push_str(" all");
        println!("{}", s);
    }
  2. Apply all modifications through the mutable reference:

    fn main() {
        let mut s = String::from("hello");
        let r = &mut s;
        r.push_str(" world");
        r.push_str(" all");
        println!("{}", r);
    }

6.3.3 Why These Rules?

They prevent data races and guarantee memory safety without a garbage collector. The compiler enforces them at compile time, ensuring there is no risk of data corruption or undefined behavior.

Though these rules may seem stringent, especially in single-threaded situations, they substantially reduce programming errors. We will delve deeper into the rationale in the following section.

Comparison with C

In C, multiple pointers can easily refer to the same data and modify it independently, often leading to unpredictable results:

#include <stdio.h>
#include <string.h>

int main() {
    char s[6] = "hello";
    char *p1 = s;
    char *p2 = s;
    strcpy(p1, "world");
    printf("%s\n", p2); // "world"
    return 0;
}

Rust’s borrow checker eliminates these kinds of issues at compile time.