1

Given a value of type Vec<&'static str>, I can freely convert that to Vec<&'r str>, as 'r is a subregion of 'static. That seems to work for most types, e.g. Vec, pairs etc. However, it doesn't work for types like Cell or RefCell. Concretely, down_vec compiles, but down_cell doesn't:

use std::cell::Cell;

fn down_vec<'p, 'r>(x: &'p Vec<&'static str>) -> &'p Vec<&'r str> {
    x
}


fn down_cell<'p, 'r>(x: &'p Cell<&'static str>) -> &'p Cell<&'r str> {
    x
}

Giving the error:

error[E0308]: mismatched types
 --> src/lib.rs:9:5
  |
9 |     x
  |     ^ lifetime mismatch
  |
  = note: expected reference `&'p std::cell::Cell<&'r str>`
             found reference `&'p std::cell::Cell<&'static str>`
note: the lifetime `'r` as defined on the function body at 8:18...
 --> src/lib.rs:8:18
  |
8 | fn down_cell<'p, 'r>(x: &'p Cell<&'static str>) -> &'p Cell<&'r str> {
  |                  ^^
  = note: ...does not necessarily outlive the static lifetime

Why does this not work for Cell? How does the compiler track that it doesn't work? Is there an alternative that can make it work?

Neil Mitchell
  • 9,090
  • 1
  • 27
  • 85
  • 2
    That's because `Cell` is invariant in the lifetime of T. See: https://doc.rust-lang.org/nightly/nomicon/subtyping.html#variance – Peter Hall May 29 '20 at 11:47
  • 2
    [This answer might help](https://stackoverflow.com/questions/33233003/how-does-the-rust-compiler-know-cell-has-internal-mutability/33233793#33233793), specifically the section on "Lifetime Variance". – SCappella May 29 '20 at 11:56

1 Answers1

3

Cell and RefCell are different because they allow mutation of the internal value through a shared reference.

To see why this is important, we can write a function that uses down_cell to leak a reference to freed memory:

fn oops() -> &'static str {
    let cell = Cell::new("this string doesn't matter");
    let local = String::from("this string is local to oops");
    let broken = down_cell(&cell);  // use our broken function to rescope the Cell
    broken.set(&local);             // use the rescoped Cell to mutate `cell`
    cell.into_inner()               // return a reference to `local`
}                                   // uh-oh! `local` is dropped here

oops contains no unsafe blocks, but it compiles, so in order to prevent accessing freed memory the compiler must reject down_cell.

The type level explanation for why this is so is because Cell<T> and RefCell<T> contain an UnsafeCell<T>, which makes them invariant in T, while Box<T> and Vec<T> are covariant in T.

The reason Vec, Box and other container-like structures can be covariant is because those containers require &mut access to mutate their contents, and &mut T is itself invariant in T. You couldn't write a function like oops using down_vec -- the compiler wouldn't allow it.

References

trent
  • 25,033
  • 7
  • 51
  • 90