Why do these ways of creating a reference behave differently?

Viewed 92

The following code runs correctly.

{
    let a = &mut 3;
    *a = 4;
    assert_eq!(*a, 4);
}

The following runs also.

{
    let a = Some(&3);
    let mut b = a.unwrap();
    assert_eq!(a.unwrap(), &3);
}

But the following can't be compiled.

{
    let a = Some(&mut 3);
    assert_eq!(*a.unwrap(), 3);
}

The error is:

40 |         let a = Some(&mut 3);
   |                           ^ - temporary value is freed at the end of this statement
   |                           |
   |                           creates a temporary which is freed while still in use
41 |         assert_eq!(*a.unwrap(), 3);
   |                     - borrow later used here
   |
   = note: consider using a `let` binding to create a longer lived value

Why Rust compiler treats those references differently that the first two pieces of code can make reference's lives more than temporary while the last one can't?

1 Answers

This is a concept in Rust called temporary lifetime extension. There are rules that govern when a temporary's lifetime is extended, though. It doesn't happen any time a borrow of a temporary occurs; the expression on the right side of a let statement needs to be a so-called "extending expression." This part of the document linked above neatly explains the first and third examples in your question:

So the borrow expressions in &mut 0, (&1, &mut 2), and Some { 0: &mut 3 } are all extending expressions. The borrows in &0 + &1 and Some(&mut 0) are not: the latter is syntactically a function call expression.

Since the expression assigned to a in the third example isn't an extending expression, the lifetime of the temporary is not extended beyond the let statement, and that causes the lifetime problem.

Why does this work for Some(&3) then? Because of constant promotion:

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

Since you borrow immutably, Rust allocates a hidden, static i32 and borrows that instead, giving you an Option<&'static i32>, which is obviously valid for the whole life of the program itself. This is not technically temporary lifetime extension because after the i32 is promoted to have static lifetime it's no longer a temporary.

It's basically equivalent to this (except that HIDDEN has no name):

static HIDDEN: i32 = 3;
let a = Some(&HIDDEN);
let mut b = a.unwrap();
assert_eq!(a.unwrap(), &HIDDEN);
Related