What is the relationship between the value of a struct and the lifetime in reference field?

struct A<'a>{
   v:&'a i32
}
fn main(){
   let i = 0;
   let a = A{v:&i};
}

The type &'a i32 implies that the immutable borrow of a value of type i32 should live for 'a. So, I wonder, what requirement does the lifetime 'a impose on the value of type A<'a>?

My understanding is that:

The value of type A<'a> should live at most for the lifetime 'a

This means, the value of A<'a> is only valid within region 'a, it can live shorter than 'a but cannot live longer. Is this interpretation right? I didn't find any principle or RFC that has a formal interpretation in this regard.

2115-argument-lifetimes - The Rust RFC Book says:

Sometimes you need to build data types that contain borrowed data. Since those types can then be used in many contexts, you can't say in advance what the lifetime of those borrows will be. Instead, you must take it as a parameter:

struct VecIter<'vec, T> {
   vec: &'vec Vec<T>,
   index: usize,
}

Here we're defining a type for iterating over a vector, without requiring ownership of that vector. To do so, we store a borrow of the vector. But because our new VecIter struct contains borrowed data, it needs to surface that fact, and the lifetime connected with it. It does so by taking an explicit 'vec parameter for the relevant lifetime, and using it within.

Validating References with Lifetimes - The Rust Programming Language says:

As with generic data types, we declare the name of the generic lifetime parameter inside angle brackets after the name of the struct so we can use the lifetime parameter in the body of the struct definition. This annotation means an instance of the struct can’t outlive the reference it holds in its part field.

So your thought is correct.

3 Likes

As a subtlety, note that lifetimes only apply to things that are borrowing data - if a value is owned, then there's nothing for the lifetime to interact with.

So A<'a> does not quite say "the value of type A<'a> should live at most for the lifetime 'a" - instead it says "the borrows contained within A are guaranteed to remain valid for the lifetime 'a". For the case of A<'a>, this does imply that A<'a> is only valid for the lifetime 'a, since it contains a borrow - but when you get to syntax of the form T<'a, A: 'a>, it becomes relevant, since A will either outlive 'a, or does not borrow data.

This particular subtlety can cause people to tie themselves in knots handling things like std::thread::spawn, which wants a 'static bound on its input. That doesn't mean that the thing it's passed has to outlive 'static, but instead that the thing it's passed cannot borrow data that has a shorter lifetime, but it's fine for it to live for less than 'static itself.

1 Like

While a useful guideline and mental model, that is not actually true.

The T: 'x relation used to be based on data reachability (logic about what is borrowed), but it is now a syntactical property.

5 Likes

@farnz @quinedot

I think we can analogy the things like:

For a given reference type & (mut)? 'a T, it means the value of the type(i.e borrowing) should live for at most lifetime 'a. Hence, a value of type Struct<'a> means the value of the type should live for at most lifetime 'a as well.

Unlike the above types, T: 'static does not mean T is constructed from 'static, in the above types, the lifetime parameters are part of these types(i.e. the complete type is constructed from the lifetime). T: 'static has a special meaning that, the bound requires type T satisfies 'static, which means, each lifetime in T should outlive 'static.

If T is not or does not contain any reference type, the requirement is a vacuous truth, I think.

1 Like

FYI rust-blog/common-rust-lifetime-misconceptions.md at master · pretzelhammer/rust-blog · GitHub

1 Like

So, my thought in What is the relationship between the value of a struct and the lifetime in reference field? - #5 by xmh0511 is basically true, Right?

fn assert_static<T: 'static>(_: T) {}

fn f<'a>(_: &'a str) {} // 'a doesn't have to outlive 'static

fn main() { assert_static(f) } // ok

You should read carefully about the misconceptions, especially

  • This is false: 2) if T: 'static then T must be valid for the entire program
    • This is true: if T: 'static then T can be a borrowed type with a 'static lifetime or an owned type
      • since T: 'static includes owned types that means T
      • can be dynamically allocated at run-time
      • does not have to be valid for the entire program
      • can be safely and freely mutated
      • can be dynamically dropped at run-time
      • can have lifetimes of different durations (here!!!)
  • This is false: 3) &'a T and T: 'a are the same thing

if T: 'static then T must be valid for the entire program

I agree this is false.

each lifetime in T should outlive 'static.

I originally meant the lifetime parameter 'a in T should satisfy 'a: 'static, instead of T. The case you mentioned does make my statement be false but I think the sentence shouldn't apply to the parameter types in a function type.

The following may be a careful list for T: 'x:

  • if T is a reference type, the lifetime ''t' in the top-level of T should satisfy 't :'a
  • if T is a struct type, each field's type F should satisfy F: 'a
  • if T is a tuple type, each element type Ei shall satisfy Ei: 'x
  • if T is an array type, the element type E shall satisfy E:'x

The way I think about it is "is valid for". Struct<'a>: 'a, so it's valid to have one within the region 'a. Struct<'a>: 'b doesn't hold unless 'a: 'b, so it's invalid to have one outside the region 'a.

Just like with references, lifetimes inferred within a function are the output of a static analysis, and that doesn't effect the drop scope of variables or anything like that. And lifetimes from outside a function must be greater than the function body.

I'm not sure what you mean by "contructed from 'static". T: 'static works just like T: 'a, e.g. for nominal types:

  • T: 'a if and only if
    • 'b: 'a for all lifetime parameters 'b of T
    • U: 'a for all type parameters U of T
  • T: 'static if and only if
    • 'b: 'static for all lifetime parameters 'b of T
    • U: 'static for all type parameters U of T

What exactly do you mean by "the requirement"? I demonstrated a type containing no reference that doesn't meet the 'static bound. Here's another with nary a &.

2 Likes

"contructed from 'x" means the lifetime is as the part of the type to form the type, such as A<'a>, the type A<'a> is constructed from A and 'a. By your utterance, that means 'x is the lifetime parameter of T.

T: 'a if and only if

  • 'b: 'a for all lifetime parameters 'b of T
  • U: 'a for all type parameters U of T

So, with the clarification of "contructed from 'x", I think we basically agree with all. However, @vague give the example to make this statement false.

fn assert_static<T: 'static>(_: T) {}

fn f<'a>(_: &'a str) {} 

fn main() { assert_static(f) } // ok

For the type of f, the lifetime 'a in fn(&'a str) does not satisfy 'a: 'static. 'a is the lifetime parameter of fn(&'a str)? I'm not sure.

What exactly do you mean by "the requirement"?

The requirement is T: 'static.

1 Like

An interesting example is this:

fn assert_static<T: 'static>(_: &T) {}

fn f<'a>(_: &'a str) {}

struct S<'a> {
    f: fn(&'a str),
}

fn main() {
    let s = S { f }; // ok
    // let s: S<'static> = S { f }; // error[E0597]: `string` does not live long enough
    
    assert_static(&s);
    let string = String::from("");
    (s.f)(&string);
}

The example doesn't make the statement false.

fn<'a>(_: &'a str) is a higher-ranked type. Function items are pretty special on their own, but we can look at the analogous function pointer type: fn(&str). That's short for for<'a> fn(&'a str). It's a subtype of fn(&'x str) for any 'x.

Next, function pointers (and items) are contravariant in their arguments, so fn(&'x str) is a subtype of fn(&'static str). So that assert_static won't fail even with a non-'static argument. You can introduce some invariance (e.g. fn(*mut &'x str)) to get it to fail, though.

Playground.

1 Like

So, I suppose the lifetime parameter in the parameter type of a function type does not count as the lifetime parameter of the function type. Otherwise, the example cannot be interpreted.

Use the formal wording, we say

T: 'a if and only if

    • 'b: 'a for all lifetime parameters 'b of T

In that example T is fn(&'a str), the trait bound is T: 'static, which equals to fn(&'a str) : 'static, so, is 'a the lifetime parameter of fn(&'a str) ?

Reread my links. The playground demonstrates they are considered. The RFC has a separate rule for function pointers that checks every input type and the output type.

You can. It's variance again. Use &mut in the assert_static to get rid of some variance.

1 Like

So, in summary

T: 'a if and only if

    • 'b: 'a for all lifetime parameters 'b of T
    • U: 'a for all type parameters U of T

The rule is suitable for all cases except for function types, the latter is a special case that is governed by variance, Right?

No, variance has nothing to do with the how the outlives relation is defined. It just means that you might coerce to a supertype that meets the outlives relation in question. See the last part of my latest playground: by sticking the S into an invariant place (inside a &mut), the coercion can't take place.

The rules are in the RFC. The one you quoted is for nominal types [1]. The one for function pointers considers all input types and the output type. The one for trait objects considers the "object type fragment" (traits) and the trait object lifetime.

They're all considering concrete lifetimes and types and variance doesn't enter into it.


  1. the RFC lists all parameters together, while I split out lifetime parameters and type parameters ↩︎

1 Like

IMO, the contravariant / variant influences the coercion, for example, fn(&'a str) is contravariant in its lifetime 'a, so, for any type fn(&'b str), if 'b: 'a(i.e. 'b is a subtype of 'a), then fn(&'a str) is a subtype of fn(& 'b str). That means, fn(&'a str) can be coerced to fn(& 'b str). However, how does coercion influence the trait bound?

Updated:

@quinedot

Seems I got the answers.

    // Since S<'a> is contraviriant in 'a, this doesn't limit inference at all
    assert_static(&s);

Since S<'a> is contravariant in 'a, 'static: 'a, hence S<'static> is a supertype of S<'a>, hence in assert_static(&s);, &s(type & S<'a>) will be coerced to & S<'static> where S<'static>: 'static is true, Right?

1214-projections-lifetimes-and-wf - The Rust RFC Book has taken that interesting example into account :laughing:

Simpler outlives relation. The older definition for the outlives relation T: 'a was rather subtle. The new rule basically says that if all type/lifetime parameters appearing in the type T must outlive 'a, then T: 'a (though there can also be other ways for us to decide that T: 'a is valid, such as in-scope where clauses). So for example fn(&'x X): 'a if 'x: 'a and X: 'a (presuming that X is a type parameter). The older rules were based on what kind of data was actually reachable, and hence accepted this type (since no data of &'x X is reachable from a function pointer). This change primarily affects struct declarations, since they may now require additional outlives bounds:

// OK now, but after this RFC requires `X: 'a`:
struct Foo<'a, X> {
    f: fn(&'a X) // (because of this field)
}