Why cannot use Copy type variable while it is mutably borrowed?


#1

Hi,

I am studying Rust again (after a long pause) and I am following the good Blandy’s Programmin Rust book, at page 118 he shows this example:

let mut y = 20;
let m1 = &mut y;
...
let z = y; // error: cannot use y because it was mutably borrowed

I cannot understand why Rust complains about usage of y, the assignment from y to z is not a move, since i32 is a Copy type. So, since it is just a bit-copy, after the assignment z and y are two completely separated things. So why bother about that?

Thank you.


#2

You can’t copy from something that’s mutably borrowed for the same reason you can’t create a shared reference at the same time as a mutable reference: aliasing. You can’t read from a value that is mutably borrowed (unless you do it through the borrow itself), as the value might be in an invaled state (supposed invariants not upheld).


#3

I’d guess it comes down to two things:

  • &i32 is allowed to be sent across threads. Therefore, creating an &i32 while a &mut i32 is held is forbidden, to prevent data races.
  • Copying a Copy type implicitly borrows it.

You can’t read from a value that is mutably borrowed (unless you do it through the borrow itself), as the value might be in an invaled state (supposed invariants not upheld).

I’m not convinced by this, for the very reason you first mention in parentheses. But I’m also a bit tied and maybe not thinking straight.


#4

Thanks for your reply.

Please note that z and y are both i32, not &i32, and about your second point, I was thinking that assignment of Copy variable is just a shallow bit copy, with no borrow, no move involved.


#5

Right, I meant to suggest that it might be a language limitation that theoretically could be lifted. (some people might not want this, though!)

Note though that if that very same assignment line appeared in a closure, it would still look like a direct move of Copy data, but in fact it would be dereferencing a pointer borrowed in the closure context.


#6

When you say that “it could be in an invalid state” this only applies in multi threading scenario, in a single thread situation, a Copy value that it has not been moved should be always in a valid state, shouldn’t?


#7

Just as a side remark, there have been times when I wrote a line like

fn foo(&self) {
    let me: Self = self.some_method();
    let _guard = &mut self;

    // do stuff with `me`.  The existence of _guard ensures that
    // I do not accidentally use `self` anymore
    ....
}

(this can be refactored into something like self.some_method()._foo() but that gets more onerous as you add arguments)


#8

There is an up and coming change called nll (non lexical lifetimes) which would allow the code as-is since m1 would have stopped being used.

No. This is key point that is undefined.
If your code contained at end;

*m1 += 1;

You would think z would be 20, but the compiler is allowed to swap around lines; so y could possibly be 21 at time z is assigned. (if it were allowable.)

The language has cell to cover cases that need to block such optimization.


#9

It’s not just multi-threading. There’s various ways this can go wrong with closures, recursive functions, etc.

Here’s a bit of a contrived example:

use std::ops::{Deref, DerefMut};

#[derive(Clone, Copy)]
pub struct NeverNone<T> {
    val: Option<T>
}

pub struct NeverNoneRef<'a, T: 'a> {
    source: &'a mut NeverNone<T>,
    val: Option<T>,
}

impl<'a, T: 'a> Drop for NeverNoneRef<'a, T> {
    fn drop(&mut self) {
        self.source.val = self.val.take()
    }
}

impl<T> NeverNone<T> {
    pub fn new(val: T) -> Self {
        NeverNone { val: Some(val) }
    }

    pub fn borrow(&mut self) -> NeverNoneRef<T> {
        let val = self.val.take();
        NeverNoneRef {
            source: self,
            val
        }
    }
}

impl<'a, T: 'a> Deref for NeverNoneRef<'a, T> {
    type Target = T;

    fn deref(&self) -> &T {
        self.val.as_ref().expect("never none")
    }
}

impl<'a, T: 'a> DerefMut for NeverNoneRef<'a, T> {
    fn deref_mut(&mut self) -> &mut T {
        self.val.as_mut().expect("never none")
    }
}

You could get a value of NeverNone { val: None } if you’d be able to copy while borrowed.


#10

In Rust, having a mutable reference to something means that there is no other way to access variable stored in it. This heavily simplifies the job of writing correct code, as you don’t have to worry about suddenly changing values, other functions reading partially modified state or even reading uninitialized memory - only the owner of a mutable reference can access the value.

Additionally, this allows for optimizations that would be really tricky to prove in other programming languages. Alias analysis is a really hard problem in compiler optimization (NP-hard, even), and Rust avoids the entire issue by not allowing aliasing.

Note that if you want a variable to be accessible while allowing someone to modify it, you can use std::cell::Cell. The purpose of this type is to warn the programmer is that the value stored in can change at any point.


#11

I’m still not sure that I buy this argument in this case. Certainly, once you have the ability to create a mutable pointer and an immutable pointer to data and hand them off to a function, all hope is lost. But the question here is about using owned Copy data in the presence of a mutable reference, without the creation of a second pointer.

…and borrowck is already capable of identifying this type of aliasing, purely through local analysis.


#12

I think the data race/correctness hazard is the important one - any performance considerations are a red herring IMO.


#13

Here’s an example of code that would crash if aliasing &mut on Copy types were allowed:

#[derive(Copy, Clone)]
enum Foo {
    IsNumber(usize),
    IsString(&'static str),
}

fn main() {
    let mut foo = Foo::IsString("hello");
    if let Foo::IsString(s) = &foo {
        // `foo` is now a number...
        foo = Foo::IsNumber(0);
        // ... but `s` still thinks that it's pointing to a string!
        println!("{}", *s);  // BOOM
    }
}

#14

That is a write in the presence of a &. I challenge somebody to make it go boom by performing a (by value) read in the presence of a &mut.


#15

One can easily imagine a morally equivalent scenario if you throw threads into the mix. Fundamentally, copying a value is a read of the location and if another thread is writing to it, you get a data race/UB. I think this was already mentioned upthread but seems to have been dismissed for some reason.


#16

But if the read is done in the non-owning thread, then that’s still reading from a borrow. And the write can’t be in the non-owning thread because…

…because…oh..

…apparently it turns out that &mut T impls Send whenever T does. Meaning the write could potentially occur in another thread…meaning that there are, in fact, very legitimate safety reasons that copying the value is forbidden.


#17

That’s exactly it and is a very useful property - you can, eg, pass a writable stack location to another thread so long as the current frame outlives that scoped thread execution.