Rust borrow checker analogy

91 views Asked by At

Being a novice C++ programmer learning Rust, I am not fully comfortable with ownership and how the borrow checker works, so while experimenting with it, I've come to an analogy that I wanted to inquire about. Could I think of rusts' transfer of ownership as the compiler inserting a free immediately after any function call that borrows any heap-allocated value, as an example:

analogy(Type* val) {
...
}

function() {
    Type* arg = new Type()
    analogy(arg)

// Could I think of the borrow checker on assumption
// as a preprocessor that inserts the following

    delete arg 

// After doing so, a static analyzer is run by the compiler
// which can trivially detect an error such as this

    print(arg)
}
2

There are 2 answers

0
CookedCthulhu On

The following explanation is a simplified model but it should get you started. new and delete are opposites, just like { and } are. It's best to think about it with lots of braces where the opening brace resembles the introduction of a fresh variable and possibly new. The closing brace represents the end of the variable's life time and possibly delete:

{
    Foo x = new ...;

    {
        Foo y = &x; // y's introduction lacks 'new'
        bar(y);     // function calls do not imply 'new'
        // so no 'delete' here
    }
    // y is inaccessible here without extending the closing brace

    bar(x);
    print(x);

    delete x;
}
// x is inaccessible here
2
Matthieu M. On

Actually, there is already an analogy in the standard library: instead of compile-time borrow-checking, you can use run-time borrow-checking with RefCell (single-threaded) or RwLock (multi-threaded).

We can thus convert an example by using RefCell, try it on the playground:

use std::cell::RefCell;

pub fn original() {
    let mut x = String::from("Hello, world!");

    let y = &x;

    //  x.push('!');  //  Borrow-checking error.

    println!("{y:?}");

    x.push('!');

    println!("{x:?}");
}

pub fn converted() {
    let x = RefCell::new(String::from("Hello, world!"));

    let y = x.borrow();

    //  x.borrow_mut().push('!');  //  Panic.

    println!("{y:?}");

    x.borrow_mut().push('!');

    println!("{x:?}");
}

Hence, rather than a temporary "delete", it's best to think of borrowing as read-write locking:

  • &T locks for reads, and multiple readers are allowed, but no writer is.
  • &mut T locks for writes, no other reader nor writer is allowed.