niko fixes
This commit is contained in:
parent
d2e802b827
commit
a1f6bbc67a
2 changed files with 126 additions and 96 deletions
|
|
@ -92,7 +92,6 @@ quite permisive with respect to other dubious operations. Rust considers it
|
|||
* Deadlock
|
||||
* Leak memory
|
||||
* Fail to call destructors
|
||||
* Access private fields
|
||||
* Overflow integers
|
||||
* Delete the production database
|
||||
|
||||
|
|
|
|||
221
lifetimes.md
221
lifetimes.md
|
|
@ -102,59 +102,11 @@ more than a local lint against incorrect usage of a value.
|
|||
|
||||
|
||||
|
||||
# Weird Lifetimes
|
||||
|
||||
Given the following code:
|
||||
|
||||
```rust
|
||||
struct Foo;
|
||||
|
||||
impl Foo {
|
||||
fn mutate_and_share(&mut self) -> &Self { &*self }
|
||||
fn share(&self) {}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut foo = Foo;
|
||||
let loan = foo.mutate_and_share();
|
||||
foo.share();
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
<anon>:11:5: 11:8 error: cannot borrow `foo` as immutable because it is also borrowed as mutable
|
||||
<anon>:11 foo.share();
|
||||
^~~
|
||||
<anon>:10:16: 10:19 note: previous borrow of `foo` occurs here; the mutable borrow prevents subsequent moves, borrows, or modification of `foo` until the borrow ends
|
||||
<anon>:10 let loan = foo.mutate_and_share();
|
||||
^~~
|
||||
<anon>:12:2: 12:2 note: previous borrow ends here
|
||||
<anon>:8 fn main() {
|
||||
<anon>:9 let mut foo = Foo;
|
||||
<anon>:10 let loan = foo.mutate_and_share();
|
||||
<anon>:11 foo.share();
|
||||
<anon>:12 }
|
||||
^
|
||||
```
|
||||
|
||||
What happened? Well, the lifetime of `loan` is derived from a *mutable* borrow.
|
||||
This makes the type system believe that `foo` is mutably borrowed as long as
|
||||
`loan` exists, even though it's a shared reference. To my knowledge, this is not
|
||||
a bug.
|
||||
|
||||
|
||||
|
||||
|
||||
# Lifetime Elision
|
||||
|
||||
In order to make common patterns more ergonomic, Rust allows lifetimes to be
|
||||
*elided* in function, impl, and type signatures.
|
||||
*elided* in function signatures.
|
||||
|
||||
A *lifetime position* is anywhere you can write a lifetime in a type:
|
||||
|
||||
|
|
@ -336,16 +288,18 @@ In Rust, subtyping derives entirely from *lifetimes*. Since lifetimes are derive
|
|||
from scopes, we can partially order them based on an *outlives* relationship. We
|
||||
can even express this as a generic bound: `T: 'a` specifies that `T` *outlives* `'a`.
|
||||
|
||||
We can then define subtyping on lifetimes in terms of lifetimes: `'a : 'b` implies
|
||||
`'a <: b` -- if `'a` outlives `'b`, then `'a` is a subtype of `'b`. This is a very
|
||||
We can then define subtyping on lifetimes in terms of lifetimes: if `'a : 'b`
|
||||
("a outlives b"), then `'a` is a subtype of `b`. This is a
|
||||
large source of confusion, because a bigger scope is a *sub type* of a smaller scope.
|
||||
This does in fact make sense. The intuitive reason for this is that if you expect an
|
||||
`&'a u8`, then it's totally fine for me to hand you an `&'static u8`, in the same way
|
||||
`&'a u8`, then it's totally fine for me to hand you an `&'static u8` in the same way
|
||||
that if you expect an Animal in Java, it's totally fine for me to hand you a Cat.
|
||||
|
||||
(Note, the subtyping relationship and typed-ness of lifetimes is a fairly arbitrary
|
||||
construct that some disagree with. I just find that it simplifies this analysis.)
|
||||
|
||||
TODO: higher rank lifetime subtyping
|
||||
|
||||
Variance is where things get really harsh.
|
||||
|
||||
Variance is a property that *type constructors* have. A type constructor in Rust
|
||||
|
|
@ -356,21 +310,27 @@ take a lifetime and a type.
|
|||
A type constructor's *variance* is how the subtypes of its inputs affects the
|
||||
subtypes of its outputs. There are three kinds of variance:
|
||||
|
||||
* F is *covariant* if `T <: U` implies `F<T> <: F<U>`
|
||||
* F is *contravariant* if `T <: U` implies `F<U> <: F<T>`
|
||||
* F is *variant* if `T` being a subtype of `U` implies `F<T>` is a subtype of `F<U>`
|
||||
* F is *invariant* otherwise (no subtyping relation can be derived)
|
||||
|
||||
(For those of you who are familiar with variance from other languages, what we refer
|
||||
to as "just" variant is in fact *covariant*. Rust does not have contravariance.
|
||||
Historically Rust did have some contravariance but it was scrapped due to poor
|
||||
interactions with other features.)
|
||||
|
||||
Some important variances:
|
||||
|
||||
* `&` is covariant (as is *const by metaphor)
|
||||
* `&` is variant (as is *const by metaphor)
|
||||
* `&mut` is invariant (as is *mut by metaphor)
|
||||
* `Fn(T)` is contravariant with respect to `T`
|
||||
* `Box`, `Vec`, and all other collections are covariant
|
||||
* `Fn(T) -> U` is invariant with respect to `T`, but variant with respect to `U`
|
||||
* `Box`, `Vec`, and all other collections are variant
|
||||
* `UnsafeCell`, `Cell`, `RefCell`, `Mutex` and all "interior mutability"
|
||||
types are invariant
|
||||
|
||||
To understand why these variances are correct and desirable, we will consider several
|
||||
examples. We have already covered why `&` should be covariant.
|
||||
examples. We have already covered why `&` should be variant when introducing subtyping:
|
||||
it's desirable to be able to pass longer-lived things where shorter-lived things are
|
||||
needed.
|
||||
|
||||
To see why `&mut` should be invariant, consider the following code:
|
||||
|
||||
|
|
@ -391,28 +351,29 @@ fn overwrite<T: Copy>(input: &mut T, new: &mut T) {
|
|||
|
||||
The signature of `overwrite` is clearly valid: it takes mutable references to two values
|
||||
of the same type, and replaces one with the other. We have seen already that `&` is
|
||||
covariant, and `'static` is a subtype of *any* `'a`, so `&'static str` is a
|
||||
variant, and `'static` is a subtype of *any* `'a`, so `&'static str` is a
|
||||
subtype of `&'a str`. Therefore, if `&mut` was
|
||||
*also* covariant, then the lifetime of the `&'static str` would successfully be
|
||||
*also* variant, then the lifetime of the `&'static str` would successfully be
|
||||
"shrunk" down to the shorter lifetime of the string, and `replace` would be
|
||||
called successfully. The string would subsequently be dropped, and `forever_str`
|
||||
would point to freed memory when we print it!
|
||||
|
||||
Therefore `&mut` should be invariant. This is the general theme of covariance vs
|
||||
invariance: if covariance would allow you to *store* a short-lived value in a
|
||||
Therefore `&mut` should be invariant. This is the general theme of variance vs
|
||||
invariance: if variance would allow you to *store* a short-lived value in a
|
||||
longer-lived slot, then you must be invariant.
|
||||
|
||||
`Box` and `Vec` are interesting cases because they're covariant, but you can
|
||||
`Box` and `Vec` are interesting cases because they're variant, but you can
|
||||
definitely store values in them! This is fine because *you can only store values
|
||||
in them through a mutable reference*! The mutable reference makes the whole type
|
||||
invariant, and therefore prevents you from getting in trouble.
|
||||
|
||||
Being covariant allows them to be covariant when shared immutably (so you can pass
|
||||
Being variant allows them to be variant when shared immutably (so you can pass
|
||||
a `&Box<&'static str>` where a `&Box<&'a str>` is expected). It also allows you to
|
||||
forever weaken the type by moving it into a weaker slot. That is, you can do:
|
||||
|
||||
```rust
|
||||
fn get_box<'a>(&'a u8) -> Box<&'a str> {
|
||||
// string literals are `&'static str`s
|
||||
Box::new("hello")
|
||||
}
|
||||
```
|
||||
|
|
@ -424,51 +385,67 @@ The variance of the cell types similarly follows. `&` is like an `&mut` for a
|
|||
cell, because you can still store values in them through an `&`. Therefore cells
|
||||
must be invariant to avoid lifetime smuggling.
|
||||
|
||||
`Fn` is the most confusing case, largely because contravariance is easily the
|
||||
most confusing kind of variance, and basically never comes up. To understand it,
|
||||
consider a function `len` that takes a function `F`.
|
||||
`Fn` is the most subtle case, because it has mixed variance. To see why
|
||||
`Fn(T) -> U` should be invariant over T, consider the following function
|
||||
signature:
|
||||
|
||||
```rust
|
||||
fn len<F>(func: F) -> usize
|
||||
where F: Fn(&'static str) -> usize
|
||||
{
|
||||
func("hello")
|
||||
}
|
||||
// 'a is derived from some parent scope
|
||||
fn foo(&'a str) -> usize;
|
||||
```
|
||||
|
||||
We require that F is a Fn that can take an `&'static str` and returns a usize. Now
|
||||
say we have a function that can take an `&'a str` (for *some* `'a`). Such a function actually
|
||||
accepts *more* inputs, since `&'static str` is a subtype of `&'a str`. Therefore
|
||||
`len` should happily accept such a function!
|
||||
This signature claims that it can handle any &str that lives *at least* as long
|
||||
as `'a`. Now if this signature was variant with respect to &str, that would mean
|
||||
|
||||
So a `Fn(&'a str)` is a subtype of a `Fn(&'static str)` because
|
||||
`&'static str` is a subtype of `&'a str`. Exactly contravariance.
|
||||
```rust
|
||||
fn foo(&'static str) -> usize;
|
||||
```
|
||||
|
||||
The variance of `*const` and `*mut` is basically arbitrary as they're not at all
|
||||
type or memory safe, so their variance is determined in analogy to & and &mut
|
||||
respectively.
|
||||
could be provided in its place, as it would be a subtype. However this function
|
||||
has a *stronger* requirement: it says that it can *only* handle `&'static str`s,
|
||||
and nothing else. Therefore functions are not variant over their arguments.
|
||||
|
||||
To see why `Fn(T) -> U` should be *variant* over U, consider the following
|
||||
function signature:
|
||||
|
||||
```rust
|
||||
// 'a is derived from some parent scope
|
||||
fn foo(usize) -> &'a str;
|
||||
```
|
||||
|
||||
This signature claims that it will return something that outlives `'a`. It is
|
||||
therefore completely reasonable to provide
|
||||
|
||||
```rust
|
||||
fn foo(usize) -> &'static str;
|
||||
```
|
||||
|
||||
in its place. Therefore functions *are* variant over their return type.
|
||||
|
||||
`*const` has the exact same semantics as &, so variance follows. `*mut` on the
|
||||
other hand can dereference to an &mut whether shared or not, so it is marked
|
||||
as invariant in analogy to cells.
|
||||
|
||||
This is all well and good for the types the standard library provides, but
|
||||
how is variance determined for type that *you* define? A struct informally
|
||||
speaking inherits the variance of its fields. If a struct `Foo`
|
||||
how is variance determined for type that *you* define? A struct, informally
|
||||
speaking, inherits the variance of its fields. If a struct `Foo`
|
||||
has a generic argument `A` that is used in a field `a`, then Foo's variance
|
||||
over `A` is exactly `a`'s variance. However this is complicated if `A` is used
|
||||
in multiple fields.
|
||||
|
||||
* If all uses of A are covariant, then Foo is covariant over A
|
||||
* If all uses of A are contravariant, then Foo is contravariant over A
|
||||
* If all uses of A are variant, then Foo is variant over A
|
||||
* Otherwise, Foo is invariant over A
|
||||
|
||||
```rust
|
||||
struct Foo<'a, 'b, A, B, C, D, E, F, G, H> {
|
||||
a: &'a A, // covariant over 'a and A
|
||||
a: &'a A, // variant over 'a and A
|
||||
b: &'b mut B, // invariant over 'b and B
|
||||
c: *const C, // covariant over C
|
||||
c: *const C, // variant over C
|
||||
d: *mut D, // invariant over D
|
||||
e: Vec<E>, // covariant over E
|
||||
e: Vec<E>, // variant over E
|
||||
f: Cell<F>, // invariant over F
|
||||
g: G // covariant over G
|
||||
h1: H // would also be covariant over H except...
|
||||
g: G // variant over G
|
||||
h1: H // would also be variant over H except...
|
||||
h2: Cell<H> // invariant over H, because invariance wins
|
||||
}
|
||||
```
|
||||
|
|
@ -497,8 +474,9 @@ correct variance and drop checking.
|
|||
|
||||
We do this using *PhantomData*, which is a special marker type. PhantomData
|
||||
consumes no space, but simulates a field of the given type for the purpose of
|
||||
variance. This was deemed to be less error-prone than explicitly telling the
|
||||
type-system the kind of variance that you want.
|
||||
static analysis. This was deemed to be less error-prone than explicitly telling
|
||||
the type-system the kind of variance that you want, while also providing other
|
||||
useful information.
|
||||
|
||||
Iter logically contains `&'a T`, so this is exactly what we tell
|
||||
the PhantomData to simulate:
|
||||
|
|
@ -526,16 +504,16 @@ However the one exception is with PhantomData. Given a struct like Vec:
|
|||
|
||||
```
|
||||
struct Vec<T> {
|
||||
data: *const T, // *const for covariance!
|
||||
data: *const T, // *const for variance!
|
||||
len: usize,
|
||||
cap: usize,
|
||||
}
|
||||
```
|
||||
|
||||
dropck will generously determine that Vec<T> does not contain any values of
|
||||
dropck will generously determine that Vec<T> does not own any values of
|
||||
type T. This will unfortunately allow people to construct unsound Drop
|
||||
implementations that access data that has already been dropped. In order to
|
||||
tell dropck that we *do* own values of type T and may call destructors of that
|
||||
tell dropck that we *do* own values of type T, and may call destructors of that
|
||||
type, we must add extra PhantomData:
|
||||
|
||||
```
|
||||
|
|
@ -700,3 +678,56 @@ Bar is *also* live. Since IterMut is always live when `next` can be called, if
|
|||
to it exist!
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Weird Lifetimes
|
||||
|
||||
Given the following code:
|
||||
|
||||
```rust
|
||||
struct Foo;
|
||||
|
||||
impl Foo {
|
||||
fn mutate_and_share(&mut self) -> &Self { &*self }
|
||||
fn share(&self) {}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut foo = Foo;
|
||||
let loan = foo.mutate_and_share();
|
||||
foo.share();
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
<anon>:11:5: 11:8 error: cannot borrow `foo` as immutable because it is also borrowed as mutable
|
||||
<anon>:11 foo.share();
|
||||
^~~
|
||||
<anon>:10:16: 10:19 note: previous borrow of `foo` occurs here; the mutable borrow prevents subsequent moves, borrows, or modification of `foo` until the borrow ends
|
||||
<anon>:10 let loan = foo.mutate_and_share();
|
||||
^~~
|
||||
<anon>:12:2: 12:2 note: previous borrow ends here
|
||||
<anon>:8 fn main() {
|
||||
<anon>:9 let mut foo = Foo;
|
||||
<anon>:10 let loan = foo.mutate_and_share();
|
||||
<anon>:11 foo.share();
|
||||
<anon>:12 }
|
||||
^
|
||||
```
|
||||
|
||||
What happened? Well, the lifetime of `loan` is derived from a *mutable* borrow.
|
||||
This makes the type system believe that `foo` is mutably borrowed as long as
|
||||
`loan` exists, even though it's a shared reference. This isn't a bug, although
|
||||
one could argue it is a limitation of the design. In particular, to know if
|
||||
the mutable part of the borrow is *really* expired we'd have to peek into
|
||||
implementation details of the function. Currently, type-checking a function
|
||||
does not need to inspect the bodies of any other functions or types.
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue