Why did I think the borrowing was over when it wasn't

use std::collections::binary_heap::PeekMut;
use std::collections::BinaryHeap;

fn a() {
    let mut heap = BinaryHeap::from([1, 2, 3, ]);
    if let Some(v) = heap.peek_mut() {
        PeekMut::pop(v);

    }

}

I thought this code should compile, but it didn't

error[E0597]: `heap` does not live long enough
  --> src\lib.rs:6:22
   |
5  |     let mut heap = BinaryHeap::from([1, 2, 3, ]);
   |         -------- binding `heap` declared here
6  |     if let Some(v) = heap.peek_mut() {
   |                      ^^^^-----------
   |                      |
   |                      borrowed value does not live long enough
   |                      a temporary with access to the borrow is created here ...
...
11 | }
   | -
   | |
   | `heap` dropped here while still borrowed
   | ... and the borrow might be used here, when that temporary is dropped and runs the destructor for type `Option<PeekMut<'_, i32>>`
   |
help: consider adding semicolon after the expression so its temporaries are dropped sooner, before the local variables declared by the block are dropped
   |
9  |     };
   |      +

So what's happend

this is indeed a confusing edge case of rust's syntax, I might even consider it a bug.

the problem is the if let expression is at the tail position of the function, thus the lifetime of the whole expression is extended, (even though the type of the if let expression is unit).

if you transform the if let to it's equivalent match form, the reason might be more obvious to understand:

fn a() {
    let mut heap = BinaryHeap::from([1, 2, 3, ]);
    match heap.peek_mut() {
        Some(v) => {
            PeekMut::pop(v);
        }
        None => {
        }
    }
}

as suggested by the compiler, adding a semicolon should fix the problem, because it turns the if let form to a non tail position statement expression.

    if let Some(v) = heap.peek_mut() {
        PeekMut::pop(v);

    }; // <--- note the semicolon
6 Likes

Here the relevant section from the reference about drop scopes that causes your snippet to fail compilation:

Temporaries that are created in the final expression of a function body are dropped after any named variables bound in the function body. Their drop scope is the entire function, as there is no smaller enclosing temporary scope.

5 Likes

That's one of the motivations to have new_temp_lifetime.

Summary

  • Introduce super let , which lets you introduce temporary bindings that have names.
    • The expression & $expr is equivalent to { super let tmp = $expr; &tmp } .
  • In Rust 2024, adjust temporary rules to remove known footguns:
    • Temporaries in match expressions are freed before testing patterns, rather than at the end of the match.
    • Temporaries in the tail expression of a block are freed before the end of the block.

src: temporary lifetimes draft RFC (v2) - HackMD

4 Likes

I think this is because the function will return the last expression, and this function should return a (). The value of the last expression happens to be (), so Rust thinks it should be returned, but the expression contains a reference to the variable created by the current function, so it fails, and adding a () at the end will also solve the problem

pub fn a<'a>() -> &'a (){
    let mut heap = BinaryHeap::from([1, 2, 3, ]);
    &if let Some(v) = heap.peek_mut() {
        PeekMut::pop(v);

    }
}
error[E0515]: cannot return reference to temporary value
 --> src\lib.rs:6:5
  |
6 |        &if let Some(v) = heap.peek_mut() {
  |   _____^-
  |  |_____|
  | ||
7 | ||         PeekMut::pop(v);
8 | ||
9 | ||     }
  | ||     ^
  | ||_____|
  |  |_____returns a reference to data owned by the current function
  |        temporary value created here

this is the evidence

pub fn a() {
    let mut heap = BinaryHeap::from([1, 2, 3, ]);
    if let Some(v) = heap.peek_mut() {
        PeekMut::pop(v);

    }
    ()
}

And now it can compile!
Thank you @nerditation for inspiring me to think of this

2 Likes

just to be pedantic, the fact the return type is unit () is just an accident, it's irrelevant to the problem. what actually matters is the entire if let expression is a tail expression, and it binds a variable to a temporary value, thus the temporary lifetime extension mechanism kicks in. see the links posted by @jofas and @vague for details.

to see why the return type is irrelevant, see a slightly different example:

fn a() -> i32 {
    let mut heap = BinaryHeap::from([1, 2, 3, ]);
    if let Some(_) = heap.peek_mut() {
        42
    } else {
        666
    }
}

and the "correct" code (also suggested by the compiler):

fn a() -> i32 {
    let mut heap = BinaryHeap::from([1, 2, 3, ]);
    let answer = if let Some(_) = heap.peek_mut() {
        42
    } else {
        666
    };
    answer
}

the key to make it compile is make the if let expression non-tail position, so the temporary values can be dropped.

1 Like

Now i understand. Thank you!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.