Ownership & Borrowing

Ownership is the basis for the memory management of Rust.

Rules

  • Every value has exactly one owner

  • Ownership can be passed on, both to functions and other types

  • The owner is responsible for removing the data from memory

  • The owner has all powers over the data and can mutate it

These rules:

  • are fundamental to Rust’s type system

  • are enforced at compile time

  • are practical in many other ways

Example

use std::fs::File;
use std::io::Write;
use std::io;

fn main() -> io::Result<()> {
    let file_create = File::create("test"); (1)

    let mut file = match file_create {  (2)
        Ok(f) => f, (3)
        Err(e) => panic!("File create failed: {}", e),
    };

    file.write_all(b"Hello World!")
}  (4)
1Tries to open a file
2Checks the Result of the opening operation
3Take ownership of the file handle
4Remove the handle and close it in the process

Ownership passing

use std::fs::File;
use std::io::Write;
use std::io;

fn main() -> io::Result<()> {
    let file_create = File::create("test");

    let file = match file_create {
        Ok(f) => f,
        Err(e) => panic!("File create failed: {}", e),
    };

    write_and_close(file) (1)
}

fn write_and_close(mut file: File) -> io::Result<()> { (1)
    file.write_all(b"Hello World!")

    (2)
}
1Ownership is passed here.
2The value dropped here.

First safety checkpoint

use std::fs::File;
use std::io::Write;
use std::io;

fn main() -> io::Result<()> {
    let file_create = File::create("test");

    let file = match file_create {
        Ok(f) => f,
        Err(e) => panic!("File create failed: {}", e),
    };

    write_and_close(file);
    write_and_close(file) (1)
}

fn write_and_close(mut file: File) -> io::Result<()> {
    file.write_all(b"Hello World!")
}
1This is illegal.

Oops!

8  |     let file = match file_create {
   |         ---- move occurs because `file` has type `std::fs::File`, which does not implement the `Copy` trait
...
13 |     write_and_close(file);
   |                     ---- value moved here
14 |     write_and_close(file)
   |                     ^^^^ value used here after move

In Rust-Lingo, this is called consuming.

The value cannot be used anymore.

Background

When calling write_and_close with file, the value is "moved" into the arguments of write_and_close.

At that moment, ownership passes to write_and_close.

main is not owner of the data anymore and thus not allowed to access or manipulate them.

References & Borrowing

Intuitively: what you own, you can borrow.


use std::fs::File;
use std::io::Write;
use std::io;

fn main() -> io::Result<()> {
    let file_create = File::create("test");

    let mut file = match file_create {
        Ok(f) => f,
        Err(e) => panic!("File create failed: {}", e),
    };

    print_filelen(&file)?;
    file.write_all(b"Hello World!")?;
    print_filelen(&file)
}

fn print_filelen(f: &File) -> io::Result<()> {
    println!("len: {:?}", f.metadata()?.len());
    Ok(())
}

Immutable references

& is the so-called "immutable" reference. They are:

  • Available multiple times

  • Always valid (always pointing to living data)

  • Never null

  • Guaranteed to never observe mutation of the pointee

Mutable Borrowing

use std::fs::File;
use std::io::Write;
use std::io;

fn main() -> io::Result<()> {
    let file_create = File::create("test");

    let mut file = match file_create {
        Ok(f) => f,
        Err(e) => panic!("File create failed: {}", e),
    };

    print_filelen(&file)?;
    write_to_file(&mut file);
    print_filelen(&file)
}

fn print_filelen(f: &File) -> io::Result<()> {
    println!("len: {:?}", f.metadata()?.len());
    Ok(())
}

fn write_to_file(f: &mut File) -> io::Result<()> {
    f.write_all(b"Hello World!")
}

Mutable references

&mut is the so-called "mutable" reference. They are:

  • Available only once at a time

  • Always valid (always pointing to living data)

  • Never null

  • Guaranteed to never alias (no two references point to the same data)

The Borrowing Rules

Values can be:

  • Borrowed immutably as often as you’d like

  • Or mutably exactly once at a time

  • The two rules are mutually exclusive.

Rust forbids shared mutability.

Types and their ownership behaviour

OwnedBorrowedMutably borrowed

i32

&i32

&mut i32

Point

&Point

&mut Point

Box<i32>

&i32

&mut i32

Collections and their ownership behaviour

OwnedBorrowedMutably borrowed

Vec<i32>

&[i32]

&mut [i32] or &mut Vec<i32>

String

&str

&mut str or &mut String

Working with moves: explicit clone

What if ownership behaviour is getting messy, but we don’t want to reference?

We can create a second copy of the data!

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

fn main() {
    let dot = Dot { x: 1, y: 2 };
    pacman(dot.clone());
    pacman(dot);
}

fn pacman(dot: Dot) {
    println!("Eating {:?}", dot);
}

Cloning is a general operation that - depending on the complexity of the data at hand - can be costly.

Working with moves: copy instead of move

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

fn main() {
    let dot = Dot { x: 1, y: 2 };
    pacman(dot);
    pacman(dot);
}

fn pacman(dot: Dot) {
    println!("Eating {:?}", dot);
}

Copy is meant for data that can be quickly copied in memory (using memcopy) and are allowed to be copied (e.g.: not File pointers).

Values that are copy follow the standard ownership rules, but they are copied when ownership is passed on.

Warning

The terminology around moves is similar, but not the same to the one used in C++, which is why you should always use Rust-Terminology: Ownership, passing on ownership and consumption.

Small quiz

drop is the function that deallocates a value immediately. What does the implementation look like?

use std::fs::File;

fn main() {
    let file = File::open("test").unwrap();
    let buffer = read_from(&file);
    drop(file);
    // do something long
}
#[inline]
fn drop<T>(_: T) {
  // take ownership, drop out of scope
}