pull/10/head
Alexis Beingessner 10 years ago committed by Manish Goregaokar
parent 70ca9a635f
commit 931728a825

@ -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

@ -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
```rust
fn foo(&'static str) -> usize;
```
So a `Fn(&'a str)` is a subtype of a `Fn(&'static str)` because
`&'static str` is a subtype of `&'a str`. Exactly contravariance.
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.
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.
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…
Cancel
Save