Funny inconsistency between &'a T and &'a mut T

#1

Example posted by Palladinium on the Discord:

struct Foo<'a>(&'a i32);
struct Bar<'a>(&'a mut i32);

fn foo_ref<'a, 'b>(foo: &'b Foo<'a>) -> &'a i32 {
    foo.0
}

fn bar_ref<'a, 'b>(bar: &'b Bar<'a>) -> &'a i32 {
    bar.0. // Note: &*bar.0 gives same error
}
error[E0623]: lifetime mismatch
 --> src/lib.rs:9:5
  |
8 | fn bar_ref<'a, 'b>(bar: &'b Bar<'a>) -> &'a i32 {
  |                         -----------     -------
  |                         |
  |                         this parameter and the return type are declared with different lifetimes...
9 |     bar.0
  |     ^^^^^ ...but data from `bar` is returned here

Can anyone explain why foo_ref compiles and bar_ref does not? It can’t have anything to do with variance, nor can I see how &T being Copy makes a difference.

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=f3664f22596396628cc32423e4e50bf3

0 Likes

#2

Lets replace &T and &mut T with something else to make this more clear in an equivalent example

playground

#[derive(Clone, Copy)]
struct CopyMe; // this represents &'a T
struct MoveMe; // this represents &'a mut T

// this represents the coercion &'a mut T -> &'a T
impl From<MoveMe> for CopyMe {
    fn from(m: MoveMe) -> CopyMe {
        CopyMe
    }
}

struct Foo(CopyMe);
struct Bar(MoveMe);

fn foo_ref(foo: &Foo) -> CopyMe {
    foo.0
}

fn bar_ref<'a, 'b>(bar: &Bar) -> CopyMe {
    bar.0.into() // Note: trying to move out of something behind a shared reference
}

Now it is obvious how Copy comes into play. Note that &'a mut T coerces to &'a T, but it also consumes &'a mut T. This means that because it isn’t Copy it must be moved out of Bar<'a>, but Bar<'a> is behind a shared reference, you can’t move out of it. But because &'a T is Copy you can move out of Foo<'a> behind a shared reference.

Normally consuming &'a mut T doesn’t matter because of reborrowing, but that doesn’t work here, because reborrowing makes the lifetime 'a shorter. And this is where the problem shows up, Rust tries to reborrow, and fails giving a lifetime error.

Note that by making bar_ref take a Bar<'a> directly works: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=22bfce73a1c189c0a2d9eabe8eb965ef

4 Likes

#3

In other words, (to add to @KrishnaSannasi answer) my justification goes like this:

The problem is

  1. 'b is unbounded in
fn bar_ref<'a, 'b>(bar: &'b Bar<'a>) -> &'a i32
  1. bar.0 is auto-dereferenced twice at compile time (check it with #![recursion_limit = “1”]) via
*bar -> (*bar).0 -> *((*bar).0) -> &(*((*bar).0))
|                   |              |
1st deref           2nd deref       reborrow 

which corresponds to

Bar<'a> -> &'a mut i32 -> i32 -> &'a i32

then for the case where 'b is strictly smaller than 'a, reborrowing (technically unsized corecion?) cannot happen for longer lifetime, otherwise would be dangerous.

In fact, to fix it if you add the constraint 'b: 'a required by UnsizedCoercion you can make sure of the correct coercion and it compiles.

1 Like