From 0cc13816d7fd8222fc7dbc07a6c402d911c30c72 Mon Sep 17 00:00:00 2001 From: Michal 'vorner' Vaner Date: Sat, 3 Nov 2018 21:05:04 +0100 Subject: [PATCH] Lifetimes: Updates to incorporate NLL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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. --- src/lifetime-mismatch.md | 64 ++++++++++++++++------- src/lifetimes.md | 109 +++++++++++++++++++++++++++++++++++---- 2 files changed, 142 insertions(+), 31 deletions(-) diff --git a/src/lifetime-mismatch.md b/src/lifetime-mismatch.md index 7ad2a844..5d7500dd 100644 --- a/src/lifetime-mismatch.md +++ b/src/lifetime-mismatch.md @@ -2,7 +2,8 @@ Given the following code: -```rust,ignore +```rust,edition2018,compile_fail +#[derive(Debug)] struct Foo; impl Foo { @@ -14,25 +15,25 @@ fn main() { let mut foo = Foo; let loan = foo.mutate_and_share(); foo.share(); + println!("{:?}", loan); } ``` -One might expect it to compile. We call `mutate_and_share`, which mutably borrows -`foo` temporarily, but then returns only a shared reference. Therefore we -would expect `foo.share()` to succeed as `foo` shouldn't be mutably borrowed. +One might expect it to compile. We call `mutate_and_share`, which mutably +borrows `foo` temporarily, but then returns only a shared reference. Therefore +we would expect `foo.share()` to succeed as `foo` shouldn't be mutably borrowed. However when we try to compile it: ```text 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 -11 | foo.share(); +12 | foo.share(); | ^^^ immutable borrow occurs here -12 | } - | - mutable borrow ends here +13 | println!("{:?}", loan); ``` What happened? Well, we got the exact same reasoning as we did for @@ -48,20 +49,21 @@ impl Foo { } fn main() { - 'b: { - let mut foo: Foo = Foo; - 'c: { - let loan: &'c Foo = Foo::mutate_and_share::<'c>(&'c mut foo); - 'd: { - Foo::share::<'d>(&'d foo); - } - } + 'b: { + let mut foo: Foo = Foo; + 'c: { + let loan: &'c Foo = Foo::mutate_and_share::<'c>(&'c mut foo); + 'd: { + Foo::share::<'d>(&'d foo); + } + println!("{:?}", loan); + } } } ``` 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 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. -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, 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 diff --git a/src/lifetimes.md b/src/lifetimes.md index e2f0cc86..bc917b14 100644 --- a/src/lifetimes.md +++ b/src/lifetimes.md @@ -1,9 +1,17 @@ # Lifetimes -Rust enforces these rules through *lifetimes*. Lifetimes are effectively -just names for scopes somewhere in the program. Each reference, -and anything that contains a reference, is tagged with a lifetime specifying -the scope it's valid for. +Rust enforces these rules through *lifetimes*. Lifetimes are named +regions of code that a reference must be valid for. Those regions +may be fairly complex, as they correspond to paths of execution +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 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 and elision of "obvious" things. -One particularly interesting piece of sugar is that each `let` statement implicitly -introduces a scope. For the most part, this doesn't really matter. However it -does matter for variables that refer to each other. As a simple example, let's -completely desugar this simple piece of Rust code: +One particularly interesting piece of sugar is that each `let` statement +implicitly introduces a scope. For the most part, this doesn't really matter. +However it does matter for variables that refer to each other. As a simple +example, let's completely desugar this simple piece of Rust code: ```rust let x = 0; @@ -85,7 +93,7 @@ z = y; Alright, let's look at some of those examples from before: -```rust,ignore +```rust,compile_fail fn as_str(data: &u32) -> &str { let s = format!("{}", data); &s @@ -169,7 +177,7 @@ our implementation *just a bit*.) How about the other example: -```rust,ignore +```rust,compile_fail let mut data = vec![1, 2, 3]; let x = &data[0]; 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 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 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 @@ -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 correct with respect to Rust's *true* semantics are rejected because lifetimes 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.