Lifetime problem and 'Cannot move out of'

In the following scenario, the compiler seemingly can't determine that foo is no longer used and can be dropped before f is called. The resulting error is error[E0505]: cannot move out of bar because it is borrowed.

When I understand correctly, the problem is that the lifetime'g which is inferred to be the lifetime of bar spans the whole main function (extends into the call of f).

Is there a way to work around this limitation? Or is this one of the examples that is only solved with polonius?

Playground

use std::sync::{Mutex, MutexGuard};

struct Bar {
    mutex: Mutex<usize>,
}

struct Foo<'g> {
    guard: MutexGuard<'g, usize>,
}

fn main() {
    let bar = Bar {
        mutex: Mutex::new(0),
    };

    let mut foo = Foo {
        guard: bar.mutex.lock().unwrap(),
    };
    *foo.guard += 1;
    // drop(foo);

    f(bar);
}

fn f(_: Bar) {}

It couldn't be, shouldn't be, and wouldn't be “solved by polonius”.

Yes, it can be dropped. By human. It should NOT be dropped by compiler.

Because this changes semantic if your program and quite significantly. Rust doesn't support “programming on eggshells” which you are proposing. And hope it never will.

No, the problem is that point where drop is called is very much part of the semantic of the program. It shouldn't be moved around willy-nilly by a compiler.

1 Like

No it can't? Dropping happens at the end of the scope, period. It can't happen early. Dropping is not the same as ending a borrow early.

1 Like

foo is not trivially droppable, because foo.guard implements Drop which can have observable side effect, so the compiler will not end foo's lifetime early. your Foo wrapper type is a red herring. you get the same error if you use the MutexGuard directly:

    let guard = bar.mutex.lock().unwrap();
    f(bar); // error, cannot move bar because it is borrowed

if the compiler reordered the drop, the program semantic is changed, so it's not a valid optimization for the compiler to do. you have to explicitly drop/move (as the commented drop call), or you must limit the scope of foo.

5 Likes

First, thank you for your question. I re-enforced my understanding a bit by reading it, compiling, and reading the error.

You only show part of the error message. The entire error message gives much more information. It discusses where the binding of bar is declared and where the borrow of the bar.mutex occurs. The error also points out at the the end of the block where the borrow might be used again where foo is dropped and runs a destructor for type Foo.

The error messages can be confusing. Multiple lines and the error is usually at the top like this..

error[E0505]: cannot move out of `bar` because it is borrowed

but the explain is at the bottom like this..

   | - borrow might be used here, when `foo` is dropped and runs the destructor for type `Foo<'_>`

just want say a little bit about how you should think of the borrow checker, in the following snippet:

struct Bar(usize);
struct Foo<'a>(&'a mut Bar);
fn main() {
    let mut bar = Bar(42);
    let foo = Foo(&mut bar); // (1) mut borrow of `bar` starts
    foo.0.0 += 1; // (2) the borrow is **used**
    let baz = bar; // (3) `bar` moved, invalidates borrowed reference
}  // (4) end of lexical scope

this code compiles, as Foo is "trivially droppable", this does NOT mean Foo has some magical compiler synthesized "no-op" drop glue (or "destructor"), this literally means there's NO drop glue for this type.

the reason this code compiles is not because the compiler reordered the code and moved the hypothetical "no-op" from (4) to somewhere before (3): there's nothing to move or reorder.

the borrow checker is NOT implemented in this way: keeping track of the beginning and "end" (read: drop) of borrows for a borrowed value and calculate the disjunction of their life spans, and when the value is moved or dropped, it checks whether or not it is outside of span of the all the borrows.

instead, it is implemented the other way around: keeping track of the status of the references, borrows are created as valid. when a value is dropped or moved, it invalidates all the references. when a reference is used, it checks whether the reference is still valid.

in the example code above, when Foo is "trivially droppable" (i.e. doesn't implement Drop), there's literally no drop glue code for it. so the program is accepted by the borrow check according to the rules.

but if you implements Drop for Foo, compiler will insert drop glue code at (4), which counts as a use of the inner mut reference to bar, (even if the Drop::drop() is a "no-op" function), but the reference is invalidated at (3), so the borrow checker reject the code and reports a violation.

on the surface, one may think a reference can "end early" before reaching the end of the lexical scope, and may have the impression that the compiler can reorder code and move the (hypothetical) drop or "destructor" of "reference types" early. but in reality there's no "drop" at all for the reference type & T and &mut T, their lifetime doesn't ends because they are "dropped" going out of scope, it's the last use of references that affects their perceived "lifetime".

1 Like

"Moves invalidate references" is useful as a mental model. However, the borrow checker is implemented by keeping track of borrows and then checking each use of values (places, really) to see if there's a conflict.

In the example, you can add a String to Foo so that it has drop glue and it still compiles; the compiler is (usually) smart enough to see when lifetimes need not be live in drop glue. Implementing Drop imposes stricter requirements.[1]


  1. somewhat tunable on unstable ↩︎

1 Like