more explanations

pull/340/head
Conrad Ludgate 3 years ago committed by Eric Huss
parent 0492daf82c
commit a43237778a

@ -42,11 +42,11 @@ fn debug<T: std::fmt::Debug>(a: T, b: T) {
}
fn main() {
let a: &'static str = "hello";
let hello: &'static str = "hello";
{
let b = String::from("world");
let b = &b; // 'b has a shorter lifetime than 'static
debug(a, b);
let world = String::from("world");
let world = &world; // 'b has a shorter lifetime than 'static
debug(hello, world);
}
}
```
@ -58,10 +58,10 @@ we might see the following error:
error[E0308]: mismatched types
--> src/main.rs:10:16
|
10 | debug(a, b);
| ^
| |
| expected `&'static str`, found struct `&'b str`
10 | debug(hello, world);
| ^
| |
| expected `&'static str`, found struct `&'b str`
```
This is over-restrictive. In this case, what we want is to accept any type that lives *at least as long* as `'b`.
@ -83,11 +83,11 @@ fn debug<T: std::fmt::Debug>(a: T, b: T) {
}
fn main() {
let a: &'static str = "hello";
let hello: &'static str = "hello";
{
let b = String::from("world");
let b = &b; // 'b has a shorter lifetime than 'static
debug(a, b); // a silently converts from `&'static str` into `&'b str`
let world = String::from("world");
let world = &world; // 'b has a shorter lifetime than 'static
debug(hello, world); // a silently converts from `&'static str` into `&'b str`
}
}
```
@ -98,26 +98,25 @@ Above, we glossed over the fact that `'static: 'b` implied that `&'static T: &'b
It's not always as simple as this example though, to understand that let's try extend this example a bit
```rust,compile_fail
fn debug<T>(a: &mut T, b: T) {
*a = b;
fn assign<T>(input: &mut T, val: T) {
*input = val;
}
fn main() {
let mut a: &'static str = "hello";
let mut hello: &'static str = "hello";
{
let b = String::from("world");
let b = &b;
debug(&mut a, b);
let world = String::from("world");
assign(&mut hello, &world);
}
}
```
This has a memory bug in it.
If this were to compile, this would have a memory bug.
If we were to expand this out, we'd see that we're trying to assign a `&'b str` into a `&'static str`,
but the problem is that as soon as `b` goes out of scope, `a` is now invalid, even though it's supposed to have a `'static` lifetime.
However, the implementation of `debug` is valid.
However, the implementation of `assign` is valid.
Therefore, this must mean that `&mut &'static str` should **not** a *subtype* of `&mut &'b str`,
even if `'static` is a subtype of `'b`.
@ -149,21 +148,21 @@ Here is a table of some other type constructors and their variances:
| | | 'a | T | U |
|---|-----------------|:---------:|:-----------------:|:---------:|
| * | `&'a T ` | covariant | covariant | |
| * | `&'a mut T` | covariant | invariant | |
| * | `Box<T>` | | covariant | |
| | `&'a T ` | covariant | covariant | |
| | `&'a mut T` | covariant | invariant | |
| | `Box<T>` | | covariant | |
| | `Vec<T>` | | covariant | |
| * | `UnsafeCell<T>` | | invariant | |
| | `UnsafeCell<T>` | | invariant | |
| | `Cell<T>` | | invariant | |
| * | `fn(T) -> U` | | **contra**variant | covariant |
| | `fn(T) -> U` | | **contra**variant | covariant |
| | `*const T` | | covariant | |
| | `*mut T` | | invariant | |
The types with \*'s are the ones we will be focusing on, as they are in
some sense "fundamental". All the others can be understood by analogy to the others:
Some of these can be explained simply in relation to the others:
* `Vec<T>` and all other owning pointers and collections follow the same logic as `Box<T>`
* `Cell<T>` and all other interior mutability types follow the same logic as `UnsafeCell<T>`
* `UnsafeCell<T>` having interior mutability gives it the same variance properties as `&mut T`
* `*const T` follows the logic of `&T`
* `*mut T` follows the logic of `&mut T` (or `UnsafeCell<T>`)
@ -177,8 +176,72 @@ For more types, see the ["Variance" section][variance-table] on the reference.
> take references with specific lifetimes (as opposed to the usual "any lifetime",
> which gets into higher rank lifetimes, which work independently of subtyping).
Ok, that's enough type theory! Let's try to apply the concept of variance to Rust
and look at some examples.
Now that we have some more formal understanding of variance,
let's go through some more examples in more detail.
```rust,compile_fail
fn assign<T>(input: &mut T, val: T) {
*input = val;
}
fn main() {
let mut hello: &'static str = "hello";
{
let world = String::from("world");
assign(&mut hello, &world);
}
}
```
And what do we get when we run this?
```text
error[E0597]: `world` does not live long enough
--> src/main.rs:9:28
|
6 | let mut hello: &'static str = "hello";
| ------------ type annotation requires that `world` is borrowed for `'static`
...
9 | assign(&mut hello, &world);
| ^^^^^^ borrowed value does not live long enough
10 | }
| - `world` dropped here while still borrowed
```
Good, it doesn't compile! Let's break down what's happening here in detail.
First let's look at the `assign` function:
```rust
fn assign<T>(input: &mut T, val: T) {
*input = val;
}
```
All it does is take a mutable reference and a value and overwrite the referent with it.
What's important about this function is that it creates a type equality constraint. It
clearly says in its signature the referent and the value must be the *exact same* type.
Meanwhile, in the caller we pass in `&mut &'static str` and `&'spike_str str`.
Because `&mut T` is invariant over `T`, the compiler concludes it can't apply any subtyping
to the first argument, and so `T` must be exactly `&'static str`.
This is counter to the `&T` case
```rust
fn debug<T: std::fmt::Debug>(a: T, b: T) {
println!("a = {:?} b = {:?}", a, b);
}
```
Where similarly `a` and `b` must have the same type `T`.
But since `&'a T` *is* covariant over `'a`, we are allowed to perform subtyping.
So the compiler decides that `&'static str` can become `&'b str` if and only if
`&'static str` is a subtype of `&'b str`, which will hold if `'static: 'b`.
This is true, so the compiler is happy to continue compiling this code.
---
First off, let's revisit the meowing dog example:
@ -249,56 +312,6 @@ enough into the place expecting something long-lived.
Here it is:
```rust,compile_fail
fn evil_feeder<T>(input: &mut T, val: T) {
*input = val;
}
fn main() {
let mut mr_snuggles: &'static str = "meow! :3"; // mr. snuggles forever!!
{
let spike = String::from("bark! >:V");
let spike_str: &str = &spike; // Only lives for the block
evil_feeder(&mut mr_snuggles, spike_str); // EVIL!
}
println!("{}", mr_snuggles); // Use after free?
}
```
And what do we get when we run this?
```text
error[E0597]: `spike` does not live long enough
--> src/main.rs:9:31
|
6 | let mut mr_snuggles: &'static str = "meow! :3"; // mr. snuggles forever!!
| ------------ type annotation requires that `spike` is borrowed for `'static`
...
9 | let spike_str: &str = &spike; // Only lives for the block
| ^^^^^^ borrowed value does not live long enough
10 | evil_feeder(&mut mr_snuggles, spike_str); // EVIL!
11 | }
| - `spike` dropped here while still borrowed
```
Good, it doesn't compile! Let's break down what's happening here in detail.
First let's look at the new `evil_feeder` function:
```rust
fn evil_feeder<T>(input: &mut T, val: T) {
*input = val;
}
```
All it does is take a mutable reference and a value and overwrite the referent with it.
What's important about this function is that it creates a type equality constraint. It
clearly says in its signature the referent and the value must be the *exact same* type.
Meanwhile, in the caller we pass in `&mut &'static str` and `&'spike_str str`.
Because `&mut T` is invariant over `T`, the compiler concludes it can't apply any subtyping
to the first argument, and so `T` must be exactly `&'static str`.
The other argument is only an `&'a str`, which *is* covariant over `'a`. So the compiler
adopts a constraint: `&'spike_str str` must be a subtype of `&'static str` (inclusive),

Loading…
Cancel
Save