The current temporary design was described and proposd in a 2014 blog post. The best description of these rules is probably this comment in the source code. We will attempt to summarize here.
Under the current rules, temporaries are typically dropped at the end of a statement:
let x = foo.lock().do_something();
// ^^^^^^ ^
// Returns a `guard`... |
// ...that is dropped here
let x = do_something(&foo.lock());
// ^^^^ ^
// | |
// | ...that is dropped here
// |
// Returns a `guard`...
However, in some circumstances, we extend the lifetime of a temporary until the end of the enclosing block. This is based on a syntactic analysis that occurs before the type-check. The basic rule is "is this temporary certain to be stored in a local variable":
let x = &foo.lock();
// ^^^^^^^^^^
// |
// Resulting guard will be stored in `x`, extend until
// end of block.
This also covers temporary that are stored into structs (or other data structures) which in turn are stored into local variables:
let x = Something {
field: &foo.lock();
// ^^^^^^^^^^
// |
// Resulting guard will be stored in `x`, extend until
// end of block (same scope as `x`).
};
The same applies to structs that are themselves temporaries stored into local variables:
let x = &Something {
// ^
// the `Something` will be stored into a temporary
// and reference to that stored into `x`
field: &foo.lock();
};
The current rules fail to detect cases where a reference to a temporary will be stored into a local variable. To some extent, that is inevitable, but there are some specific situations that occur quite often.
One particular failing of the current goals is that they are not abstractable. For eaxmple, we saw that we can do let x = Something { ... }
and extend the lifetime for temporaries that appear in its fields; but if we have a constructor like Something::new
, that doesn't work (playground):
struct Something<'me> {
name: &'me str,
}
impl<'me> Something<'me> {
fn new(name: &'me str) -> Something<'me> {
Self { name }
}
}
fn main() {
let x = Something::new(&String::new());
// ^^^^^^^^^^^ ^
// | ...dropped here
// Temporary created...
println!("{}", x.name);
// ^^^^ out of scope here
}
Or, as the compiler puts it:
error[E0716]: temporary value dropped while borrowed
--> src/main.rs:12:29
|
12 | let x = Something::new(&String::new());
| ^^^^^^^^^^^^^ - temporary value is freed at the end of this statement
| |
| creates a temporary which is freed while still in use
13 | println!("{}", x.name);
| ------ borrow later used here
|
= note: consider using a `let` binding to create a longer lived value
It'd be nice if this worked.
We will analyze the signature of each function to identify extended parameters. These are parameters where:
&'a T
for some T
'a
is constrained by the return type…
'b
that is constrained by the return type.This analysis can be done based purely on the declared signature of the function and doesn't require any kind of inference or type checking, just "type collecting".
fn foo<'a, 'b>(
input: &'a String, // Yes! Appears in return type.
output: &'b String, // No! Does not, cannot be returned.
) -> &'a str {
input
}
fn foo<'a, 'b>(
input: &'a String, // Yes! Appears in return type.
output: &'b String, // Yes! Appears in return type.
) -> (&'a str, &'b str) {
input
}
fn foo<'a, 'b>(
input: &'a String,
output: &'b String, // Yes! Outlives 'a, which appears in return type.
) -> &'a str
where
'b: 'a,
{
output
}
Note that appearing in the return type is not enough; the lifetime must be constrained by the return type. These are the same rules we use to determine what is a valid impl (see e.g. these tests):
trait Mirror<'a> {
type Output;
}
fn foo<'a, 'b>(
input: &'a String, // No, appears in return type, but not constrained by it.
output: &'b String, // No, appears in return type, but not constrained by it.
) -> <&'a str as Mirror<'b>>::Output
where
'b: 'a,
{
...
}
We will extend the grammar E&
of expressions considered as extension expressions from
E& = & ET
| StructName { ..., f: E&, ... }
| [ ..., E&, ... ]
| ( ..., E&, ... )
| {...; E&}
| box E&
| E& as ...
| ( E& )
to include the i
th parameter to a call (i.e., E& = E (E0 ... E(i-1), E&, ... )
when the signature of the callee E
is analyzed and the i
th parameter is a extended parameter (per the rules just described). For the purposes of this check, method calls like foo.bar(...)
consider foo
to be the 0th parameter.
Currently the set of "extended lifetime temporaries" is determined before type-check, based purely on a walk of the HIR. The new rules require us to move that reasoning into the type-check itself. We likely want to do this by:
check_expr
take a new parameter indicating whether they are in a "extended temporary" position.I don't think so, because I think that the only cases it covers are cases that would be type errors otherwise. But I may be wrong! We should devise a way to test this experimentally on crater (e.g., applying both analyses and reporting an error somehow).
Maybe, but I suspect that existing lints (or proposed lints) will mostly cover the concerns. That said, some lints like this one may no longer apply, we should double check that! (i.e., this code may now work as intended? Perhaps not, because it is using raw pointers.)