Bug or just unhelpful error message?

I seem to have got myself confused over this program:

fn main() {
  let mut x = Box::new(0);
  let mut y = Box::new(&mut x);
  y = Box::new({*y});
}

The error message on the playground is:

error[E0506]: cannot assign to `y` because it is borrowed
 --> src/main.rs:4:3
  |
4 |   y = Box::new(*y);
  |   ^            -- borrow of `y` occurs here
  |   |
  |   assignment to borrowed `y` occurs here
  |   borrow later used here

Somehow this doesn't make sense to me since I'm not borrowing y at all! In fact, I don't see a problem with this program. Note it is the same if {*y} becomes *y.

This works: y = Box::new(*{y});

1 Like

Nice!! Ok, so what about this:

fn main() {
  let mut x1 = Box::new(1);
  let mut x2 = Box::new(2);
  let mut y = Box::new((&mut x1,&mut x2));
  println!("GOT {},{}",y.0,y.1);
  y = Box::new((y.1,y.0));
  println!("NOW {},{}",y.0,y.1);
}

(I was trying to construct a more realistic example after the initial one above)

As a workaround, you can always do something like

fn main() {
  let mut x1 = Box::new(1);
  let mut x2 = Box::new(2);
  let mut y = Box::new((&mut x1,&mut x2));
  println!("GOT {},{}",y.0,y.1);
  y = Box::new({let y=y; (y.1,y.0)});
  println!("NOW {},{}",y.0,y.1);
}

Edit: Or alternatively

fn main() {
  let mut x1 = Box::new(1);
  let mut x2 = Box::new(2);
  let mut y = Box::new((&mut x1,&mut x2));
  println!("GOT {},{}",y.0,y.1);
  y = Box::new((|| (y.1,y.0))());
  println!("NOW {},{}",y.0,y.1);
}
1 Like

Haha, that is also a nice work-around. However, I guess the question remains why the original doesn't work given that it seems correct. Somehow the borrow checker is not reasoning about partial borrows and their "flow on" effects.

Ok, the second one above is quite interesting. I can't understand why that works but the original doesn't ??

Well, it seems like the closure captures by move. In that sense it is quite similar to the first workaround which also works by making very explicit that we first want to move out of y.

Hmmmmm, surely it should be moving either way. The first workaround works because it forces a move of the entire variable, no? Where as in this case, it is indeed figuring out that moving two pieces of the variable means the whole variable is gone (well at least no longer usable). So, if it can do this here, then it can do it without the closure .. no?

Interesting: This compiles

fn main() {
  let mut x1 = Box::new(1);
  let mut x2 = Box::new(2);
  let mut y = Box::new((&mut x1,&mut x2));
  println!("GOT {},{}",y.0,y.1);
  let (mut z1, mut z2) = (Box::new(10), Box::new(20));
  let mut z = Box::new((&mut z1, &mut z2));
  z = Box::new((y.1,y.0));
  println!("NOW {},{}",z.0,z.1);
  println!("NOW {},{}",y.0,y.1);
}

While this doesn’t:

#[derive(Debug)]
struct Loud(i32);
impl Drop for Loud {
    fn drop(&mut self) {
        println!("someone’s shouting {}!", self.0);
    }
}
fn main() {
  let mut x1 = Box::new(1);
  let mut x2 = Box::new(2);
  let mut y = Box::new((&mut x1,&mut x2));
  println!("GOT {},{}",y.0,y.1);
  let (mut z1, mut z2) = (Box::new(10), Box::new(20));
  let mut z = Box::new((&mut z1, &mut z2));
  let z = Box::new((y.1,y.0));
  println!("NOW {},{}",z.0,z.1);
  println!("NOW {},{}",y.0,y.1);
}

So it looks like

let z = Box::new((y.1,y.0));

moves out of y.1 and y.0 while

z = Box::new((y.1,y.0)); // where `z` was already initialized

only re-borrows them.

1 Like

Which opens two more workarounds using a (super useful in general)

macro_rules! move_ {
    ($e:expr) => {
        {let __x = $e; __x}
    }
}

macro:

this

fn main() {
  let mut x1 = Box::new(1);
  let mut x2 = Box::new(2);
  let mut y = Box::new((&mut x1,&mut x2));
  println!("GOT {},{}",y.0,y.1);
  y = move_!(Box::new((y.1,y.0)));
  println!("NOW {},{}",y.0,y.1);
}

or this

fn main() {
  let mut x1 = Box::new(1);
  let mut x2 = Box::new(2);
  let mut y = Box::new((&mut x1,&mut x2));
  println!("GOT {},{}",y.0,y.1);
  y = Box::new(move_!((y.1,y.0)));
  println!("NOW {},{}",y.0,y.1);
}
1 Like

Ok, yes, that macro is quite interesting to me ... thanks!!!

That macro has two useful effects: (a) force a move, (b) all temporaries inside it are dropped as soon as it’s fully evaluated (i.e. earlier than only at the end of the enclosing statement).

So, if we can force a move ... can we force a copy?

Of course. A copy is just a move on a Copy type, right? When I was saying "force a move" I pretty much meant "force a move/copy depending on the type". Just like the move keyword on a clusure means "capture by move or copy depending on the type".

1 Like

This is mainly a question of type inference vs. reborrows: Rust conservatively tries to reborrow when it knows that the types involved are references, whereas when type inference is involved in a way where it is not initially obvious there is a "borrow" type to be inferred, then Rust operates off move semantics and by the time it infers that borrow / reference types were involved, "it is too late" to reborrow.

That's why let z = ... moves (type of z is fully inferred), whereas an assignment reborrows (type of z is known to be that of a reference type, by the time the assignment happens).

I'd expect the let z case to reborrow, should it be completely type-annotated.


The usual workarounds for when the reborrowing logic actually causes the program not to compile are those mentioned by @steffahn: immediate-called closures, that thus capture by value, or the body of their move_! macro.

2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.