Lifetimes: Updates to incorporate NLL

* Updated the explanations around lifetimes a bit.
* Made sure the examples that should fail still fail in edition 2018.
* Prefer `rust,compile_fail` instead of `rust,ignore` ‒ the latter
  allows the user to click on button and see the actual compile errors.
  Also, this'll tell us if something stops failing.
pull/101/head
Michal 'vorner' Vaner 6 years ago
parent fb29b147be
commit 0cc13816d7
No known key found for this signature in database
GPG Key ID: F700D0C019E4C66F

@ -2,7 +2,8 @@
Given the following code: Given the following code:
```rust,ignore ```rust,edition2018,compile_fail
#[derive(Debug)]
struct Foo; struct Foo;
impl Foo { impl Foo {
@ -14,25 +15,25 @@ fn main() {
let mut foo = Foo; let mut foo = Foo;
let loan = foo.mutate_and_share(); let loan = foo.mutate_and_share();
foo.share(); foo.share();
println!("{:?}", loan);
} }
``` ```
One might expect it to compile. We call `mutate_and_share`, which mutably borrows One might expect it to compile. We call `mutate_and_share`, which mutably
`foo` temporarily, but then returns only a shared reference. Therefore we borrows `foo` temporarily, but then returns only a shared reference. Therefore
would expect `foo.share()` to succeed as `foo` shouldn't be mutably borrowed. we would expect `foo.share()` to succeed as `foo` shouldn't be mutably borrowed.
However when we try to compile it: However when we try to compile it:
```text ```text
error[E0502]: cannot borrow `foo` as immutable because it is also borrowed as mutable error[E0502]: cannot borrow `foo` as immutable because it is also borrowed as mutable
--> src/lib.rs:11:5 --> src/main.rs:12:5
| |
10 | let loan = foo.mutate_and_share(); 11 | let loan = foo.mutate_and_share();
| --- mutable borrow occurs here | --- mutable borrow occurs here
11 | foo.share(); 12 | foo.share();
| ^^^ immutable borrow occurs here | ^^^ immutable borrow occurs here
12 | } 13 | println!("{:?}", loan);
| - mutable borrow ends here
``` ```
What happened? Well, we got the exact same reasoning as we did for What happened? Well, we got the exact same reasoning as we did for
@ -48,20 +49,21 @@ impl Foo {
} }
fn main() { fn main() {
'b: { 'b: {
let mut foo: Foo = Foo; let mut foo: Foo = Foo;
'c: { 'c: {
let loan: &'c Foo = Foo::mutate_and_share::<'c>(&'c mut foo); let loan: &'c Foo = Foo::mutate_and_share::<'c>(&'c mut foo);
'd: { 'd: {
Foo::share::<'d>(&'d foo); Foo::share::<'d>(&'d foo);
} }
} println!("{:?}", loan);
}
} }
} }
``` ```
The lifetime system is forced to extend the `&mut foo` to have lifetime `'c`, The lifetime system is forced to extend the `&mut foo` to have lifetime `'c`,
due to the lifetime of `loan` and mutate_and_share's signature. Then when we due to the lifetime of `loan` and `mutate_and_share`'s signature. Then when we
try to call `share`, and it sees we're trying to alias that `&'c mut foo` and try to call `share`, and it sees we're trying to alias that `&'c mut foo` and
blows up in our face! blows up in our face!
@ -69,9 +71,31 @@ This program is clearly correct according to the reference semantics we actually
care about, but the lifetime system is too coarse-grained to handle that. care about, but the lifetime system is too coarse-grained to handle that.
TODO: other common problems? SEME regions stuff, mostly?
# Improperly reduced borrows
This currently fails to compile, because Rust doesn't understand that the borrow
is no longer needed and conservatively falls back to using a whole scope for it.
This will eventually get fixed.
```rust,edition2018,compile_fail
# use std::collections::HashMap;
# use std::cmp::Eq;
# use std::hash::Hash;
fn get_default<'m, K, V>(map: &'m mut HashMap<K, V>, key: K) -> &'m mut V
where
K: Clone + Eq + Hash,
V: Default,
{
match map.get_mut(&key) {
Some(value) => value,
None => {
map.insert(key.clone(), V::default());
map.get_mut(&key).unwrap()
}
}
}
```
[ex2]: lifetimes.html#example-aliasing-a-mutable-reference [ex2]: lifetimes.html#example-aliasing-a-mutable-reference

@ -1,9 +1,17 @@
# Lifetimes # Lifetimes
Rust enforces these rules through *lifetimes*. Lifetimes are effectively Rust enforces these rules through *lifetimes*. Lifetimes are named
just names for scopes somewhere in the program. Each reference, regions of code that a reference must be valid for. Those regions
and anything that contains a reference, is tagged with a lifetime specifying may be fairly complex, as they correspond to paths of execution
the scope it's valid for. in the program. There may even be holes in these paths of execution,
as it's possible to invalidate a reference as long as it's reinitialized
before it's used again. Types which contain references (or pretend to)
may also be tagged with lifetimes so that Rust can prevent them from
being invalidated as well.
In most of our examples, the lifetimes will coincide with scopes. This is
because our examples are simple. The more complex cases where they don't
coincide are described below.
Within a function body, Rust generally doesn't let you explicitly name the Within a function body, Rust generally doesn't let you explicitly name the
lifetimes involved. This is because it's generally not really necessary lifetimes involved. This is because it's generally not really necessary
@ -23,10 +31,10 @@ syrup even -- around scopes and lifetimes, because writing everything out
explicitly is *extremely noisy*. All Rust code relies on aggressive inference explicitly is *extremely noisy*. All Rust code relies on aggressive inference
and elision of "obvious" things. and elision of "obvious" things.
One particularly interesting piece of sugar is that each `let` statement implicitly One particularly interesting piece of sugar is that each `let` statement
introduces a scope. For the most part, this doesn't really matter. However it implicitly introduces a scope. For the most part, this doesn't really matter.
does matter for variables that refer to each other. As a simple example, let's However it does matter for variables that refer to each other. As a simple
completely desugar this simple piece of Rust code: example, let's completely desugar this simple piece of Rust code:
```rust ```rust
let x = 0; let x = 0;
@ -85,7 +93,7 @@ z = y;
Alright, let's look at some of those examples from before: Alright, let's look at some of those examples from before:
```rust,ignore ```rust,compile_fail
fn as_str(data: &u32) -> &str { fn as_str(data: &u32) -> &str {
let s = format!("{}", data); let s = format!("{}", data);
&s &s
@ -169,7 +177,7 @@ our implementation *just a bit*.)
How about the other example: How about the other example:
```rust,ignore ```rust,compile_fail
let mut data = vec![1, 2, 3]; let mut data = vec![1, 2, 3];
let x = &data[0]; let x = &data[0];
data.push(4); data.push(4);
@ -201,7 +209,7 @@ violate the *second* rule of references.
However this is *not at all* how Rust reasons that this program is bad. Rust However this is *not at all* how Rust reasons that this program is bad. Rust
doesn't understand that `x` is a reference to a subpath of `data`. It doesn't doesn't understand that `x` is a reference to a subpath of `data`. It doesn't
understand Vec at all. What it *does* see is that `x` has to live for `'b` to understand `Vec` at all. What it *does* see is that `x` has to live for `'b` to
be printed. The signature of `Index::index` subsequently demands that the be printed. The signature of `Index::index` subsequently demands that the
reference we take to `data` has to survive for `'b`. When we try to call `push`, reference we take to `data` has to survive for `'b`. When we try to call `push`,
it then sees us try to make an `&'c mut data`. Rust knows that `'c` is contained it then sees us try to make an `&'c mut data`. Rust knows that `'c` is contained
@ -213,3 +221,82 @@ totally ok*, because it keeps us from spending all day explaining our program
to the compiler. However it does mean that several programs that are totally to the compiler. However it does mean that several programs that are totally
correct with respect to Rust's *true* semantics are rejected because lifetimes correct with respect to Rust's *true* semantics are rejected because lifetimes
are too dumb. are too dumb.
# The area covered by a lifetime
The lifetime (sometimes called a *borrow*) is *alive* from the place it is
created to its last use. The borrowed thing needs to outlive only borrows that
are alive. This looks simple, but there are few subtleties.
The following snippet compiles, because after printing `x`, it is no longer
needed, so it doesn't matter if it is dangling or aliased (even though the
variable `x` *technically* exists to the very end of the scope).
```rust,edition2018
let mut data = vec![1, 2, 3];
let x = &data[0];
println!("{}", x);
// This is OK, x is no longer needed
data.push(4);
```
However, if the value has a destructor, the destructor is run at the end of the
scope. And running the destructor is considered a use obviously the last one.
So, this will *not* compile.
```rust,edition2018,compile_fail
#[derive(Debug)]
struct X<'a>(&'a i32);
impl Drop for X<'_> {
fn drop(&mut self) {}
}
let mut data = vec![1, 2, 3];
let x = X(&data[0]);
println!("{:?}", x);
data.push(4);
// Here, the destructor is run and therefore this'll fail to compile.
```
Furthermore, there might be multiple possible last uses of the borrow, for
example in each branch of a condition.
```rust,edition2018
# fn some_condition() -> bool { true }
let mut data = vec![1, 2, 3];
let x = &data[0];
if some_condition() {
println!("{}", x); // This is the last use of `x` in this branch
data.push(4); // So we can push here
} else {
// There's no use of `x` in here, so effectively the last use is the
// creation of x at the top of the example.
data.push(5);
}
```
And a lifetime can have a pause in it. Or you might look at it as two distinct
borrows just being tied to the same local variable. This often happens around
loops (writing a new value of a variable at the end of the loop and using it for
the last time at the top of the next iteration).
```rust,edition2018
let mut data = vec![1, 2, 3];
// This mut allows us to change where the reference points to
let mut x = &data[0];
println!("{}", x); // Last use of this borrow
data.push(4);
x = &data[3]; // We start a new borrow here
println!("{}", x);
```
Historically, Rust kept the borrow alive until the end of scope, so these
examples might fail to compile with older compilers. Also, there are still some
corner cases where Rust fails to properly shorten the live part of the borrow
and fails to compile even when it looks like it should. These'll be solved over
time.

Loading…
Cancel
Save