Temporary Values, Borrowing, and Lifetimes
Problem§
Prior to Rust v1.61.0, the std::io::stdio::Stdin::lock
function had a signature that looked like this:
Lifetimes un-elided, it looks like:
which translates to the following:
- The returned
StdinLock
is valid for the lifetime'a
- The lifetime
'a
originates from a borrow of aStdin
struct.
It also implies that the
StdinLock
must be dropped before mutably borrowing theStdin
struct, and that we can't drop theStdin
struct before dropping theStdinLock
struct.
So what happens when we use lock
?
use io;
pub fn stdin() -> Stdin
, btw.
A wild compile error appears!
error[E0716]: temporary value dropped while borrowed
--> src/main.rs:3:14
|
3 | let locked = io::stdin().lock();
| ^^^^^^^^^^^ - temporary value is freed at the end of this statement
| |
| creates a temporary which is freed while still in use
4 | }
| - borrow might be used here, when `locked` is dropped and runs the destructor for type `StdinLock<'_>`
|
= note: consider using a `let` binding to create a longer lived value
But where did the borrow come from? Well, when calling a method with an
&self
parameter with an owned value, the Rust compiler can implicitly borrow the owned value to call the&self
method.
Explanation§
- A borrow is against an owned value. That is, borrows can't be conjured from thin air. Borrows aren't pointers. You can't hold a value with a borrow, you need an owner backing it.
- A borrow can only last as long as the owned value. It can't live after the owned value is dropped.
- Temporaries are dropped at the end of a statement (except when they're not), while values stored in
let
s drop at the end of a block.
For more information on dropping behaviour, see Drop Order.
For a much more concise explanation of the problem, I defer to the original issue for changing std::io::Stdin::lock
:
The explanation is that the lock behaves as if it borrows the original handle from stdin(), and the temporary value created for the call to the lock() method is dropped at the end of the statement, invalidating the borrow.
-- tlyu
Reproducing the problem§
Why don't you encounter the same problem when borrowing in other places? Most reference parameters to functions don't have their lifetimes tied to the output in the same way. This compiles just fine:
while the de-elided (delided?) signature of as_str()
is pub fn as_str(&self) -> &str
, which also returns a reference with the same lifetime as the input..
However! If we add but a return argument to print_str
...
The output:
abc
What. I totally thought I was going to prove my point there. What if I...
Aha!
error[E0716]: temporary value dropped while borrowed
--> src/main.rs:7:20
|
7 | let s = print_str(String::from("abc").as_str());
| ^^^^^^^^^^^^^^^^^^^ - temporary value is freed at the end of this statement
| |
| creates a temporary which is freed while still in use
8 | dbg!(s);
| - borrow later used here
|
help: consider using a `let` binding to create a longer lived value
|
7 ~ let binding = String::from("abc");
8 ~ let s = print_str(binding.as_str());
|
Our borrowed value (reference) can only live as long as the value we borrow against, and in this case, that temporary is dropped at the end of the statement. Previously, the Rust compiler didn't give us an error because we weren't using the borrow later, and the lifetime of the borrow could be the same as the temporary.
But in this case, the reference that print_str
returns has the sameish lifetime as the one that as_str
returns, which is limited by the temporary String
.
Once you start learning about all the weird tricks the Rust compiler has in order to make lifetimes convenient, you get uncomfortable calling two lifetimes "equal" or the "same".
The solution is exactly what the compiler suggests: as soon as we put our owned value in a let
statement, it will be dropped at the end of scope and we can freely borrow against it until then.
Retrospective§
So how did the Rust community change Stdin::lock
so that users no longer faced this error? They changed the signature to pub fn lock(&self) -> StdinLock<'static>
, which decouples the lifetime relationship, allowing the returned StdinLock
to outlive the &self
reference. How they did that is left to reader.
Further Reading§
- Variance and Covariance, and how it relates to Rust lifetimes - they can shrink.
- Pretzelhammer's article on Common Rust Lifetime Misconceptions will blow your mind if you've only read the Book so far.
- fasterthanlime's "A Rust match made in hell" explores a similar problem related to how match expression scrutinees can extend temporary lifetimes, and how that conflicts with locks.
- The section discussing current rules in Niko Matsaki's similarly titled "Temporary Lifetimes"
Feel free to write to me to point out an error, suggest a topic, or just say hi!