kirelagin
kirelagin

Reputation: 13616

The “outlives” relation and actual scopes

I was going through the legendary RFC 1214 and it seems that I’m missing something crucial.

struct Foo;

struct Bar<'a> {
   foo: &'a Foo
}


fn f<'x, 'y>(_: &'x Foo, _: &'y Bar<'x>)
     where 'y: 'x, 'x: 'y {
}

fn g<'x>(x: &'x Foo) {
   let y = Bar {foo : x};
   f(x, &y);  // ?
}


fn main(){
   let x = Foo;
   g(&x);
}

In this code I went to great lengths to make sure that 'x : 'y and not 'y : 'x. The function that defines x calls the function that defines y, I believe this is already enough to guarantee that x outlives y, but I also put a reference to x inside y, just to make sure.

Now, the constraints in f are such that the invocation of this function can’t possibly be valid. I mean, well, it can, if and only if 'x == 'y, but it totally looks like x lives strictly longer than y, as it is defined in the outer scope.

Nevertheless, this code typechecks and compiles. How is this possible?

Upvotes: 2

Views: 227

Answers (2)

huon
huon

Reputation: 102106

Lifetimes have variance, that is, the compiler can choose to shorten the lifetime of a &'a Foo to some &'b Foo. The lifetime of a reference like that just means that the Foo lasts at least as long as 'a: a shorter lifetime still satisfies this guarantee. This is what is happening here: the 'x lifetime is being shortened to have the same lifetime as the &y reference.

You can use invariance to stop this compiling: if the lifetime 'x cannot be shortened, then the code will stop compiling as you expect.

use std::cell::Cell;

struct Foo;

struct Bar<'a> {
   foo: Cell<&'a Foo>
}


fn f<'x, 'y>(_: Cell<&'x Foo>, _: &'y Bar<'x>)
     where 'y: 'x, 'x: 'y {
}

fn g<'x>(x: Cell<&'x Foo>) {
   let y = Bar {foo : x.clone()};
   f(x, &y);  // ?
}


fn main(){
   let x = Foo;
   g(Cell::new(&x));
}
<anon>:16:10: 16:11 error: `y` does not live long enough
<anon>:16    f(x, &y);  // ?
                   ^
<anon>:14:28: 17:2 note: reference must be valid for the lifetime 'x as defined on the block at 14:27...
<anon>:14 fn g<'x>(x: Cell<&'x Foo>) {
<anon>:15    let y = Bar {foo : x.clone()};
<anon>:16    f(x, &y);  // ?
<anon>:17 }
<anon>:15:34: 17:2 note: ...but borrowed value is only valid for the block suffix following statement 0 at 15:33
<anon>:15    let y = Bar {foo : x.clone()};
<anon>:16    f(x, &y);  // ?
<anon>:17 }

What is happening here is Cell<T> is invariant in T, because it is readable and writable. This in particular means that Cell<&'x Foo> cannot be shortened to Cell<&'y Foo>: filling it with a reference &'y Foo that is truly 'y (i.e. only lasts for 'y) will mean the reference is dangling once the cell leaves 'y (but is still in 'x).

Upvotes: 7

user395760
user395760

Reputation:

Here are three things which in combination explain the behavior you see:

  • The 'x on f is a completely different, independent lifetime parameter from the 'x in g. The compiler can choose different concrete lifetimes to substitute for each.
  • 'x : 'y, 'y: 'x means that 'x == 'y (this is not real syntax).
  • If you have a reference, you can implicitly create another reference with a shorter lifetime from it. Consider for example the function fn mangle_a_string<'a>(_s: &'a str) -> &'a str { "a static string" }

So what happens in f(x, &y) is that the first argument is coerced to a reference with a shorter lifetime, matching the second argument's lifetim, to satisfy the bounds in the where clause.

Upvotes: 5

Related Questions