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:

pub fn lock(&self) -> StdinLock<'_>

Lifetimes un-elided, it looks like:

pub fn lock<'a>(&'a self) -> StdinLock<'a>

which translates to the following:

  • The returned StdinLock is valid for the lifetime 'a
  • The lifetime 'a originates from a borrow of a Stdin struct.

It also implies that the StdinLock must be dropped before mutably borrowing the Stdin struct, and that we can't drop the Stdin struct before dropping the StdinLock struct.

So what happens when we use lock?

use std::io;
fn main() {
  let locked = io::stdin().lock();
}

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 lets 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:

fn print_str (s: &str) {
	println!("{}", s);
}

fn main () {
	print_str(String::from("abc").as_str());
}

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...

fn print_str (s: &str) -> &str {
	println!("{}", s);
	s
}

fn main () {
	print_str(String::from("abc").as_str());
}

The output:

abc

What. I totally thought I was going to prove my point there. What if I...

fn print_str (s: &str) -> &str {
	println!("{}", s);
	s
}

fn main () {
	let s = print_str(String::from("abc").as_str());
	dbg!(s);
}

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§



As always, feel free to write to me to point out an error, suggest a topic, or just say hi!