rustborrow-checkertemporary

Why is "the temporary is part of an expression at the end of a block" an error?


This is likely a textbook case of me not understanding some of the technicalities of the borrow checker, but it would be nice if someone could clear this up for me.

I have this (incredibly simplified) chunk of code, which compiles perfectly fine.

pub struct Example(pub Vec<String>);

impl Example {
  pub fn iter(&self) -> impl Iterator<Item=&String> {
    self.0.iter()
  }
}

pub fn some_condition(_: &str) -> bool {
  // This is not important.
  return false;
}

pub fn foo() -> bool {
  let example = Example(vec!("foo".to_owned(), "bar".to_owned()));
  let mut tmp = example.iter();
  tmp.all(|x| some_condition(x))
}

pub fn main() {
  println!("{}", foo());
}

However, the first thing that I tried (which, in my mind, should be equivalent to the above), was eliding the temporary variable tmp altogether, as follows

pub fn foo() -> bool {
  let example = Example(vec!("foo".to_owned(), "bar".to_owned()));
  example.iter().all(|x| some_condition(x))
}

But this version produces the following error.

error[E0597]: `example` does not live long enough
  --> so_temporary.rs:23:3
   |
23 |   example.iter().all(|x| some_condition(x))
   |   ^^^^^^^-------
   |   |
   |   borrowed value does not live long enough
   |   a temporary with access to the borrow is created here ...
24 | }
   | -
   | |
   | `example` dropped here while still borrowed
   | ... and the borrow might be used here, when that temporary is dropped and runs the destructor for type `impl std::iter::Iterator`
   |
   = note: The temporary is part of an expression at the end of a block. Consider forcing this temporary to be dropped sooner, before the block's local variables are dropped. For example, you could save the expression's value in a new local variable `x` and then make `x` be the expression at the end of the block.

Now, obviously, the note at the end of the error is an excellent suggestion, and it's why I introduced the temporary to fix the problem. But I don't understand why that fixes the problem. What's different about the lifetimes of my tmp variable versus example.iter() embedded into the expression directly, that makes one work and one fail?


Solution

  • This behavior has changed in the 2024 Edition of Rust. Block return expression will no longer extend temporaries beyond the block. The shown code produces no errors with edition = "2024".


    This has essentially the same answer as Why do I get "does not live long enough" in a return value? and its somewhat explained in the error itself, but I'll elaborate. This behavior is the same with a normal block expression:

    pub struct Example(pub Vec<String>);
    
    impl Example {
        pub fn iter(&self) -> impl Iterator<Item=&String> {
            self.0.iter()
        }
    }
    
    pub fn main() {
        let foo = {
            let example = Example(vec!("foo".to_owned(), "".to_owned()));
            example.iter().all(String::is_empty)
        };
        println!("{}", foo);
    }
    
    error[E0597]: `example` does not live long enough
      --> src/main.rs:12:9
       |
    12 |         example.iter().all(String::is_empty)
       |         ^^^^^^^-------
       |         |
       |         borrowed value does not live long enough
       |         a temporary with access to the borrow is created here ...
    13 |     };
       |     -- ... and the borrow might be used here, when that temporary is dropped and runs the destructor for type `impl Iterator`
       |     |
       |     `example` dropped here while still borrowed
       |
       = note: the temporary is part of an expression at the end of a block;
               consider forcing this temporary to be dropped sooner, before the block's local variables are dropped
    help: for example, you could save the expression's value in a new local variable `x` and then make `x` be the expression at the end of the block
       |
    12 |         let x = example.iter().all(String::is_empty); x
       |         ^^^^^^^                                     ^^^
    

    The scope of temporary values is often the statement in which they were created. In the code above example is a variable and it is destroyed at the end of the block. However, example.iter() creates a temporary impl Iterator and its temporary scope is the full let foo = ... statement. So the steps when evaluating this are:

    You can probably see where this can go wrong. The reason introducing a variable works is because it forces any temporaries to be dropped sooner. The case is slightly different when talking about functions, but the effect is the same:

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

    Regarding the comments:

    From looking at various Rust issues relating to this, it is clear that many would consider this behavior confusing enough that it is being changed in the 2024 Edition.