rustmoveborrow-checkerownershipownership-semantics

Why does this "cannot move out of `self.x` which is behind a mutable reference" error happen?


I'm trying to write Tetris in rust. I have structs in this project that I want to treat as immutable even though they do mutate.

The approach I'm using to achieve this kind of behavior is this:

#[derive(Debug)]
struct Example {
    foo: i8
}

impl Example {
    fn change(mut self) -> Self {
        self.foo = 8;
        self
    }
}

which allows you to do stuff like this:

let first = Example { foo: 0 };
let second = first.change();

println!("{:?}", second); // Example { foo: 8 }

but yells at you when you do things like this:

let first = Example { foo: 0 };
let second = first.change();
    
println!("{:?}", first); // error[E0382]: borrow of moved value: `first`

The part where I'm confused is, why does this work:

#[derive(Debug)]
struct Matrix {
    cells: [[char; 2]; 2]
}

impl Matrix {
    fn new() -> Self {
        Matrix {
            cells: [['░'; 2]; 2]
        }    
    }
    
    fn solidify(mut self, row: usize, column: usize) -> Self {
        self.cells[row][column] = '█';
        self
    }
}

fn main() {
    let matrix = Matrix::new();
    let matrix = matrix.solidify(0, 0);
    
    println!("{:?}", matrix); // Matrix { cells: [['█', '░'], ['░', '░']] }
}

when this doesn't?

#[derive(Debug)]
struct Matrix {
    cells: [[char; 2]; 2]
}

impl Matrix {
    fn new() -> Self {
        Matrix {
            cells: [['░'; 2]; 2]
        }    
    }
    
    fn solidify(mut self, row: usize, column: usize) -> Self {
        self.cells[row][column] = '█';
        self
    }
}

#[derive(Debug)]
struct Tetris {
    matrix: Matrix
}

impl Tetris {
    fn new() -> Self {
        Tetris {
            matrix: Matrix::new()
        }
    }
    
    fn change(&mut self) {
        self.matrix = self.matrix.solidify(0, 0); 
/*      -----------------------------------------
        This is where it yells at me ^                 */
    } 
}

fn main() {
    let mut tetris = Tetris::new();
    tetris.change();
    
    println!("{:?}", tetris); // error[E0507]: cannot move out of `self.matrix` which is behind a mutable reference
}

Playground

This gives:

error[E0507]: cannot move out of `self.matrix` which is behind a mutable reference
  --> src/main.rs:32:23
   |
32 |         self.matrix = self.matrix.solidify(0, 0); 
   |                       ^^^^^^^^^^^ -------------- `self.matrix` moved due to this method call
   |                       |
   |                       move occurs because `self.matrix` has type `Matrix`, which does not implement the `Copy` trait
   |
note: `Matrix::solidify` takes ownership of the receiver `self`, which moves `self.matrix`
  --> src/main.rs:13:21
   |
13 |     fn solidify(mut self, row: usize, column: usize) -> Self {
   |                     ^^^^

I've done some research and I feel like either std::mem::swap, std::mem::take, or std::mem::replace,

could do the trick for me, but I'm not exactly sure how.


Solution

  • You're right. mem::[take,replace]() can do the work.

    The problem is that while you can leave a variable uninitialized for a time, you cannot leave a mutable reference uninitialized for a time (by moving out of it), even if you reassign it after.

    There is a reason to this limitation: panics. If matrix.solidify() panics, we will exit without executing the recurring assignment to matrix. Later, we could recover from the panic, and observe the moved-from matrix.

    Without dependencies (and unsafe code), the only solution is to leave something behind even when we reassign, so that even if we panic matrix stays initialized. std::mem::take() can help with that if Matrix implements Default - it leaves the default value, while the more general std::mem::replace() can help otherwise - it leaves some value:

    #[derive(Debug, Default)]
    struct Matrix {
        cells: [[char; 2]; 2]
    }
    
    impl Tetris {
        fn change(&mut self) {
            let matrix = std::mem::take(&mut self.matrix);
            self.matrix = matrix.solidify(0, 0); 
        } 
    }
    

    Or:

    #[derive(Debug)] // No `Default`.
    struct Matrix {
        cells: [[char; 2]; 2]
    }
    
    impl Tetris {
        fn change(&mut self) {
            let matrix = std::mem::replace(&mut self.matrix, Matrix::new());
            self.matrix = matrix.solidify(0, 0); 
        } 
    }
    

    If this is not good enough for you (for example, because you don't have a good default value to insert, or because of performance requirements) you can use the replace_with crate. It provides replace_with::replace_with_or_abort(), that will just abort the whole process in case of a panic, preventing the possibility of recovering:

    impl Tetris {
        fn change(&mut self) {
            replace_with::replace_with_or_abort(&mut self.matrix, |matrix| matrix.solidify(0, 0));
        }
    }
    

    Note that instead of what you're doing now, you may actually want interior mutability.