When is it necessary to circumvent Rust’s borrow checker?

When is it necessary to circumvent Rust’s borrow checker?

It is needed when:

  • the borrow checker is not advanced enough to see that your usage is safe
  • you do not wish to (or cannot) write the code in a different pattern

As a concrete case, the compiler cannot tell that this is safe:

let mut array = [1, 2];
let a = &mut array[0];
let b = &mut array[1];

The compiler doesn’t know what the implementation of IndexMut for a slice does at this point of compilation (this is a deliberate design choice). For all it knows, arrays always return the exact same reference, regardless of the index argument. We can tell that this code is safe, but the compiler disallows it.

You can rewrite this in a way that is obviously safe to the compiler:

let mut array = [1, 2];
let (a, b) = array.split_at_mut(1);
let a = &mut a[0];
let b = &mut b[0];

How is this done? split_at_mut performs a runtime check to ensure that it actually is safe:

fn split_at_mut(&mut self, mid: usize) -> (&mut [T], &mut [T]) {
    let len = self.len();
    let ptr = self.as_mut_ptr();

    unsafe {
        assert!(mid <= len);

        (from_raw_parts_mut(ptr, mid),
         from_raw_parts_mut(ptr.offset(mid as isize), len - mid))
    }
}

For an example where the borrow checker is not yet as advanced as it can be, see What are non-lexical lifetimes?.

I borrow self.board mutably in the iter_mut() method and then try to borrow it again immutably to get all the neighbours of the read cell.

If you know that the references don’t overlap, then you can choose to use unsafe code to express it. However, this means you are also choosing to take on the responsibility of upholding all of Rust’s invariants and avoiding undefined behavior.

The good news is that this heavy burden is what every C and C++ programmer has to (or at least should) have on their shoulders for every single line of code they write. At least in Rust, you can let the compiler deal with 99% of the cases.

In many cases, there’s tools like Cell and RefCell to allow for interior mutation. In other cases, you can rewrite your algorithm to take advantage of a value being a Copy type. In other cases you can use an index into a slice for a shorter period. In other cases you can have a multi-phase algorithm.

If you do need to resort to unsafe code, then try your best to hide it in a small area and expose safe interfaces.

Above all, many common problems have been asked about (many times) before:

Leave a Comment