Search code examples
rustreferencetemporary

Why doesn't Rust increase the scope for this temporary variable?


Why does Rust not extend the scope for the temporary value referenced by val3 in the following code:

fn main() {  
    let mut vec = vec![1, 2,3];  
  
    let val1 = vec.get(0).unwrap_or(&0);  
    println!("{val1}");  
  
    let val2 = match vec.get_mut(0) {  
        Some(v) => v,  
        None => &mut 0,  
    };  
    println!("{val2}");  
  
    let val3 = vec.get_mut(0).unwrap_or(&mut 0);
    println!("{val3}");
}

Compiling it gives the following error

error[E0716]: temporary value dropped while borrowed
  --> src/main.rs:13:46
   |
13 |     let val3 = vec.get_mut(0).unwrap_or(&mut 0);
   |                                              ^ - temporary value is freed at the end of this statement
   |                                              |
   |                                              creates a temporary value which is freed while still in use
14 |     println!("{val3}");
   |               ------ borrow later used here
   |
help: consider using a `let` binding to create a longer lived value
   |
13 ~     let binding = 0;
14 ~     let val3 = vec.get_mut(0).unwrap_or(&mut binding);
   |

For more information about this error, try `rustc --explain E0716`.
error: could not compile `question` (bin "question") due to 1 previous error

Looking at the code for val1, this will work for an immutable reference using unwrap_or(). Looking at the code for val2, this will work for a mutable reference using a match expression. But clearly it doesn't work with val3 for a mutable reference using unwrap_or(). Does anyone know the reason for the difference?

If I had to take a wild guess, I'd say that the match expression works because the mutable reference to the temporary value immediately gets assigned to the variable in the let statement. When using unwrap_or(), that reference to the temporary value gets passed to the function instead. If the reference is an immutable one, this doesn't consume the reference and the assignment still works. If the reference is a mutable one, unwrap_or() consumes the reference, and there's no reborrowing because something something temporary values.


Solution

  • The short answer is that it doesn't work in the val3 case because this doesn't meet the criteria for temporary lifetime extension.

    First, let's tackle the val2 case. That is covered by this section from the above link:

    For a let statement with an initializer, an extending expression is an expression which is one of the following:

    • ...
    • The final expression of any extending block expression.

    This is the "final statement" of a match block arm, so this meets the criteria. The lifetime of the temporary is therefore extended to match the lifetime of val2.

    In the val3 case, it just doesn't meet the criteria -- function call expressions are absent from the list (which is explicitly called out in the following paragraph of the documentation).

    So why does it work in the val1 case? Since that takes a shared reference instead of an exclusive one, it is eligible for constant promotion.

    Promotion of a value expression to a 'static slot occurs when the expression could be written in a constant, borrowed, and dereferencing that borrow where the expression was originally written, without changing the runtime behavior.

    In other words, it's effectively syntactic sugar for:

    static ZERO: i32 = 0;
    let val1 = vec.get(0).unwrap_or(&ZERO);
    

    If you ran this function many times, all of the &0 references would therefore point to the same actual value, which is promoted to static-duration storage. Since you can't mutate the value through a shared reference, there is no problem doing this.

    This cannot work with exclusive references -- each exclusively-borrowed temporary needs to be unique because you can mutate them, so they can't just be shoved into static-duration storage and shared.