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?
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:
example.iter().all(...)
example
foo
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.