Search code examples
rustmutexmutability

Where is a MutexGuard if I never assign it to a variable?


I don't understand "where" the MutexGuard in the inner block of code is. The mutex is locked and unwrapped, yielding a MutexGuard. Somehow this code manages to dereference that MutexGuard and then mutably borrow that object. Where did the MutexGuard go? Also, confusingly, this dereference cannot be replaced with deref_mut. Why?

use std::sync::Mutex;

fn main() {
    let x = Mutex::new(Vec::new());
    {
        let y: &mut Vec<_> = &mut *x.lock().unwrap();
        y.push(3);
        println!("{:?}, {:?}", x, y);
    }

    let z = &mut *x.lock().unwrap();
    println!("{:?}, {:?}", x, z);
}

Solution

  • Summary: because *x.lock().unwrap() performs an implicit borrow of the operand x.lock().unwrap(), the operand is treated as a place context. But since our actual operand is not a place expression, but a value expression, it gets assigned to an unnamed memory location (basically a hidden let binding)!

    See below for a more detailed explanation.


    Place expressions and value expressions

    Before we dive in, first two important terms. Expressions in Rust are divided into two main categories: place expressions and value expressions.

    • Place expressions represent a value that has a home (a memory location). For example, if you have let x = 3; then x is a place expression. Historically this was called lvalue expression.
    • Value expressions represent a value that does not have a home (we can only use the value, there is no memory location associated with it). For example, if you have fn bar() -> i32 then bar() is a value expression. Literals like 3.14 or "hi" are value expressions too. Historically these were called rvalue expressions.

    There is a good rule of thumb to check if something is a place or value expression: "does it make sense to write it on the left side of an assignment?". If it does (like my_variable = ...;) it is a place expression, if it doesn't (like 3 = ...;) it's a value expression.

    There also exist place contexts and value contexts. These are basically the "slots" in which expressions can be placed. There are only a few place contexts, which (usually, see below) require a place expression:

    • Left side of a (compound) assignment expression (⟨place context⟩ = ...;, ⟨place context⟩ += ...;)
    • Operand of an borrow expression (&⟨place context⟩ and &mut ⟨place context⟩)
    • ... plus a few more

    Note that place expressions are strictly more "powerful". They can be used in a value context without a problem, because they also represent a value.

    (relevant chapter in the reference)

    Temporary lifetimes

    Let's build a small dummy example to demonstrate a thing Rust does:

    struct Foo(i32);
    
    fn get_foo() -> Foo {
        Foo(0)
    }
    
    let x: &Foo = &get_foo();
    

    This works!

    We know that the expression get_foo() is a value expression. And we know that the operand of a borrow expression is a place context. So why does this compile? Didn't place contexts need place expressions?

    Rust creates temporary let bindings! From the reference:

    When using a value expression in most place expression contexts, a temporary unnamed memory location is created initialized to that value and the expression evaluates to that location instead [...].

    So the above code is equivalent to:

    let _compiler_generated = get_foo();
    let x: &Foo = &_compiler_generated;
    

    This is what makes your Mutex example work: the MutexLock is assigned to a temporary unnamed memory location! That's where it lives. Let's see:

    &mut *x.lock().unwrap();
    

    The x.lock().unwrap() part is a value expression: it has the type MutexLock and is returned by a function (unwrap()) just like get_foo() above. Then there is only one last question left: is the operand of the deref * operator a place context? I didn't mention it in the list of place contests above...

    Implicit borrows

    The last piece in the puzzle are implicit borrows. From the reference:

    Certain expressions will treat an expression as a place expression by implicitly borrowing it.

    These include "the operand of the dereference operator (*)"! And all operands of any implicit borrow are place contexts!

    So because *x.lock().unwrap() performs an implicit borrow, the operand x.lock().unwrap() is a place context, but since our actual operand is not a place, but a value expression, it gets assigned to an unnamed memory location!

    Why doesn't this work for deref_mut()

    There is an important detail of "temporary lifetimes". Let's look at the quote again:

    When using a value expression in most place expression contexts, a temporary unnamed memory location is created initialized to that value and the expression evaluates to that location instead [...].

    Depending on the situation, Rust chooses memory locations with different lifetimes! In the &get_foo() example above, the temporary unnamed memory location had a lifetime of the enclosing block. This is equivalent to the hidden let binding I showed above.

    However, this "temporary unnamed memory location" is not always equivalent to a let binding! Let's take a look at this case:

    fn takes_foo_ref(_: &Foo) {}
    
    takes_foo_ref(&get_foo());
    

    Here, the Foo value only lives for the duration of the takes_foo_ref call and not longer!

    In general, if the reference to the temporary is used as an argument for a function call, the temporary lives only for that function call. This also includes the &self (and &mut self) parameter. So in get_foo().deref_mut(), the Foo object would also only live for the duration of deref_mut(). But since deref_mut() returns a reference to the Foo object, we would get a "does not live long enough" error.

    That's of course also the case for x.lock().unwrap().deref_mut() -- that's why we get the error.

    In the deref operator (*) case, the temporary lives for the enclosing block (equivalent to a let binding). I can only assume that this is a special case in the compiler: the compiler knows that a call to deref() or deref_mut() always returns a reference to the self receiver, so it wouldn't make sense to borrow the temporary for only the function call.