Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do lifetime bounds on structs work in Rust?

Tags:

rust

lifetime

There was some discussion of this in IRC yesterday, which left me feeling vaguely dissatisfied.

The question was:

How do you define a lifetime on a struct to restrict its contents to only things that live as long as 'itself'.

i.e. a 'self sort of thing.

My initial reaction was: you can't.

If you create a struct Foo<'a>, the lifetime associated with it is inferred from the references it contains; unless the struct contains a reference to itself (impossible), you can't have this sort of 'self lifetime.

There was a bunch of chatter about it, and I ended up writing this playground as a result:

#[derive(Debug)]
struct Bar;

#[derive(Debug)]
struct Foo<'a> {
  a:&'a Bar,
  b:&'a Bar
}

fn factory<'a>(v1:&'a Bar, v2: &'a Bar) -> Foo<'a> {
  return Foo {
    a: v1,
    b: v2
  };
}

fn main() { // <---- Let's call this block lifetime 'one
  let a = Bar; 
  let c = &a; // <-- C has lifetime 'one
  { // <------------ Let's call this block lifetime 'two
    let b = Bar;

    let mut foo1 = factory(c, c);  
    foo1.b = &b;

    let mut foo2 = factory(&b, &b);  
    foo2.a = &a;

    println!("{:?}", foo1);
    println!("{:?}", foo2);
  }
}

However, I'm now more confused rather than less.

So, in a strict sense in the above:

  • c has 'one
  • &b has 'two
  • 'static > 'one > 'two (that is, 'two is bounded by 'one).
  • foo1 has 'one
  • foo2 has 'two

Now, my confusion:

Foo<'a> indicates that 'a is the minimum lifetime bound that can be contained by the instance of Foo.

  • Since 'one > 'two, foo2 should be able contain a &'one a; this works.

  • Since 'two > 'one, foo1 should not be able to contain &'two b; however, this works.

Why?

It would appear my confusion results from one of two misconceptions; either:

  1. The instance of foo1 is in fact Foo<'two>, not Foo<'one>.

    I don't understand why this would be the case, since it is manufactured in factory<'a> where <'a> is the lifetime of c; which is 'one, not 'two. There's absolutely no way c can be &'two in the example above. The lifetime 'two is not available in the function factory where Foo is created.

2) Struct lifetimes don't work how I understand them to work; i.e. a lifetime of 'a on a Foo instance can somehow change between after the instance is created (e.g. on a move?)

...but I don't know which one.

like image 463
Doug Avatar asked Dec 23 '14 00:12

Doug


1 Answers

Lifetime parameters on references are covariant: they can be substituted with a shorter lifetime when necessary.

Basically, you've got it the wrong way around. When you have a &'one Bar, you cannot assign a reference to a value with a shorter lifetime (such as 'two here), otherwise the reference would be dangling when execution leaves the 'two scope. However, when you have a &'two Bar, you can assign a reference to a value with a longer lifetime (such as 'one and 'static), because the reference will go out of scope before the referent does.

Why does your program compile? The compiler doesn't only use information from the calls to factory to select the appropriate lifetime; it also uses information from the assignments. &a has type &'one Bar and &b has type &'two Bar. Because 'two starts after 'one and ends before 'one, the compiler can coerce a &'one Bar to a &'two Bar. In object-oriented terms, a &'one Bar is a &'two Bar (&'one Bar is a subtype of &'two Bar). Just like how in Java, you can pass a String as an argument to a function that expects an Object. The subtyping relation for classes in Java is that child classes are subtypes of their parents, but the subtyping relation for lifetimes is that longer lifetimes are subtypes of shorter ones.

This means that we've found a common type for &a and &b: &'two Bar. Therefore, the compiler infers 'two for 'a in the calls to factory.

Note that the type of foo2 doesn't change at the assignment; the type of a value is always static.

like image 175
Francis Gagné Avatar answered Sep 21 '22 12:09

Francis Gagné