How to understand "temporary lifetime extension" correctly?

The following code is known to compile:

let foo = &String::new();
foo;

And the following code is known to fail to compile:

let bar = String::new().as_bytes();
bar;

Getting error message:

error[E0716]: temporary value dropped while borrowed
 --> src/main.rs
  |
  |     let bar = String::new().as_bytes();
  |               ^^^^^^^^^^^^^           - temporary value is freed at the end of this statement
  |               |
  |               creates a temporary which is freed while still in use
  |     bar;
  |     --- borrow later used here
  |
  = note: consider using a `let` binding to create a longer lived value

I have read the documentation: Destructors - The Rust Reference. But this seems to require memorizing a lot of special rules to judge whether the temporary lifetime is extended.

Is there a way to understand these rules without going to mechanical memory?

2 Likes

Please see this response from another thread:

Hope you find it useful

2 Likes

Basically the syntax

let foo = &bar;

has special meaning and puts the expression bar in a local variable to keep it alive.

5 Likes

To give a simple explanation:

When you do String::new(), you create an owned value. Then, .as_bytes() creates a reference from it. Rust requires references to point to a valid data in the memory.

The problem is, if you do String::new().as_bytes(), the String::new() part is dropped after the statement has ended, because we didn't save it anywhere. So it just "disappears" in the memory. Which means we're left with the result of .as_bytes() which is now a dangling pointer.

This is why you need a let binding ; by doing let foo = String::new(); let bar = foo.as_bytes();, you store the value of String::new() in a variable so it's guaranteed to be saved somewhere in the memory, and .as_bytes() will actually refer to this variable's content instead of a dropped value.

Hope my explanation is clear :slight_smile:

1 Like

The most important rule is for the simple let foo = &bar; case. As long as you also know there's some other cases, you'll just have to keep in mind that "whenever code compiles even though it shouldn't, due to a temporary being used for longer than what seems appropriate, then it's either a case of temporary lifetime extension or of static promotion".

You don't have to rely on temporary lifetime extension in the code you write. It also (mostly) only affects code that wouldn't compile without it, so it rarely makes a surprising difference in behavior. (In my opinion it's no more surprising than any of the ordinary rules for temporary scopes, with e. g. the differences between if EXPR { ... } vs if let true = EXPR { ... }.)

When reading code, you can often ignore the scopes of temporaries anyways; and for types where dropping is important, like e. g. mutex guards, code structured in a way that makes the scope obvious is best practice anyways. This can be by assigning the value to a local variable, or even by using an explicit call to drop.

2 Likes

I've been thinking about this.
Why can't the same be done when bar is being used in some other arbitrary expression? That is, why not generalize?

Well, the rule that allows this to work is purely syntax-based, and it doesn't know the lifetime characteristics of the functions you called. You wouldn't want String::new().len() to keep the string around until the end of the scope, but syntax-wise they are the same.

Making this kind of thing work without being syntax based is very complicated because doing the lifetime computations generally happens after the behavior of the code (including where destructors run) has been set in stone.

2 Likes

In general this would be a breaking change, and when it isn't it could create very surprising situations. For example if the temporary is a mutex lock then it would be very surprising if it was kept alive until the scope surrounding the statement ends, creating potential for panics or deadlocks.

2 Likes

I was thinking more that it could live until just after its last use though, rather than until the end of the block (scope is a somewhat subjective term, as what I'm saying below would effectively shorten the scope).
Putting aside the feasibility of that, which would no doubt require a more sophisticated (and expensive) borrowck analysis algorithm, could that work conceptually? I think it would for your mutex example, as well as the String::new().len() example provided by @alice.

This mitigates the issue, but doesn't solve it entirely. Extending the lifetime of a mutex guard is a potential footgun even if only until its last use. Moreover it creates another problem, now you may have destructors running in any part of a block, not just at its end.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.