Search code examples
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:

    • evaluate the result of example.iter().all(...)
    • drop example
    • assign result to foo
    • drop impl Iterator

    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:

    • The reason it works when impl Iterator is replaced with std::slice::Iter<'_, i32> (in pretzelhammer's example) is because the drop checker knows that slice::Iter doesn't access example on drop whereas it has to assume that impl Iterator does.

    • The reason it works with fn my_all(mut self, ...) (in Peter Hall's example) is because all takes the iterator by reference but my_all takes it by value. The temporary impl Iterator is consumed and destroyed before the end of the expression.

    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.