Good explanation of 'static

@AA_BB, I think it's a fair, pragmatic summary, even though being precise about these things get quite complicated (therefore my initial need to explain it in simpler terms). However, a small tip:

I would refer to 'static as either a "bound" as in T: 'static or a concrete "lifetime" as in &'static str, and mention that they're not the same. The 'static bound can "accept" &'static references or objects with an unrestricted lifetime (meaning it can live until the program ends). Saying that a type is 'static makes it a bit unclear which of them you're talking about (the bound or the concrete lifetime) and might cause some unneeded confusion.

My own two cents, which fits in a simple sentence:

T : 'lifetime iff any value of type T can be held (owned) / used during the whole 'lifetime safely.

For instance, if we take 'lifetime = 'static, the lifetime that never ends, this means that T : 'static if you can hold / use any value of type T for as long as you want.

That's why, for instance, String : 'static, even when you will almost never see a &'static String anywhere: even though, in practice, the vast majority of String instances are dropped at some point, it remains true that if you own a String, and want to keep owning it until the end of times / never free it, then you can. When you "own" a &'short_lived i32, on the other hand, even if you are owning that reference, Rust won't let you keep using it beyond the end of 'short_lived.

And that's kind of the surprising part for Rust newcomers: this : 'static property is implicitly wired in their brain to hold for all types, since that's what happens in most other languages, and they just consider borrows to be some kind of special-cased type in the language.

And it is not. A borrow is like any other type, but one that happens to be infected with a lifetime parameter (the lifetime of the borrow). And when a type is infected with a lifetime parameter, Rust won't let you use any instance of that type beyond the end of that lifetime.

  • Hence the "practical" rule about : 'static :
    T : 'static iff there are no non-'static borrows inside T.

  • This is what justifies that thread::spawn require that the closure be 'static: the closure may be running ("used") for arbitrarily long, so we need that the actual closure instance given to spawn never dangle. And, as usual, a property on the type of such values is easier to check at compile-time rather than a property on the values: that's why spawn requires that all the instances of that type never dangle, i.e., that their type be : 'static (which does lead to Rust refusing to compile safe programs).

Exercice

Make the following code compile:

trait Object {}
impl<T> Object for T {}

fn type_erasure<T> (boxed: Box<T>)
  -> Box<dyn Object>
{
    boxed
}
  • Remember: in order to coerce a value of type Box<T> to a
    Box<dyn Trait + 'lifetime>, we need to have:

    • T : Trait (which in the example is verified thanks to the blanket impl);

    • T : 'lifetime.

  • Solution 1: you can do what the error message suggests…

    … and add a : 'static bound on T, preventing it from having any borrows.

    But, as mentioned in this thread, when dealing with function signatures, the more properties you yield (-> Box<dyn Object + 'static> in the return type is : 'static), the more difficult it is to meet them in the implementation, or you may end up yourself being overly restrictive in your input.

    And this is what happened here: you ended up requiring that T : 'static beforehand, just in case the caller needs the returned boxed object to be : 'static. But the caller may not need that, they may be content with a less powerful return type in exchange of a looser entry point. And with <T : 'static>, we don't give the caller such a choice.


  • Solution 2: a less restrictive solution

    When wanting to give options to the caller, the trick is to use and maybe even introduce generic parameters, since generic parameters are always chosen by the caller.

    So you can, for instance, let the caller pick the lifetime they want in the return type:

    //              added
    //              vvv
    fn type_erasure<'lt, T> (x: T)
      -> Box<dyn Object + 'lt>
    //                  ^^^^^
    //                  added
    {
        Box::new(x)
    }
    

    which still fails but this time the suggested solution is to add a T : 'lt bound, which makes the code compile; and this time we have a generic version of the previous solution: the caller can still choose 'lt = 'static and have the same usability as the first solution, but they can also choose any other lifetime, provided it be "smaller" than all the lifetimes contained within T, and they will have a boxed object that is usable during that lifetime.


  • Crazier and crazier digressions off this last solution

    Since T : 'lt is a property about "the area of owned-usability of T being contained within the 'lt "area", a nice tool to better understand T : 'lt would be to have a way to directly express the area of owned-usability of T. I'm gonna imagine that it is written 'T.

    • T : 'lt ⇔ 'T : 'lt ⇔ 'lt βŠ† 'T

    If this was possible, then the above signature, for instance, and many others, would become simpler:

    fn type_erasure<T> (boxed: Box<T>)
      -> Box<dyn Object + 'T>
    
    • Incidentally, this leads to the observation that most dyn-based mechanisms for type-erasure are unable to perform lifetime-erasure (at least for unconstrained generics). Or, basically, they can only erase down to that area of usability (so that a type with multiple lifetime parameters can be type-erased to one "area of owned-usability" (aside: that are would be contained within the intersection of the multiple lifetimes).

    And so, the practical observation here is that, even though we are not able to express practical "area of owned-usability" constructs such as 'T (or 'a β‹‚ 'b / min('a, 'b) for the intersection), one can usually circumvent that limitation using generics, and based on the fact that the lifetime solver in Rust only looks for existential solutions:

    So, by introducing <'t where T : 't>, we can write the rest of the code as if 't = 'T, and stuff will Just Workβ„’.

    Similarly, we can write <'c where 'a : 'c, 'b : 'c> so as to consider that 'c = 'a β‹‚ 'b.

    Incorrect vision

    Many people consider that 'a + 'b expresses the 'a β‹‚ 'b constraint

    • For instance, they would expect:

      /// Incorrect.
      fn type_erasure<'a, 'b> (boxed: Box<(&'a (), &'b ())>)
        -> Box<dyn 'a + 'b + Object>
      

      to compile.

    and this is wrong: 'a + 'b is just a combination of two bounds whose semantics are equivalent to a single bound over the union / the maximum of 'a and 'b:

    • Set view: 'T : 'a + 'b, iff ('T : 'a and 'T : 'b), i.e., ('a βŠ† 'T and 'b βŠ† 'T), iff 'a U 'b βŠ† 'T, i.e., 'T : 'a U 'b

    • Ordering view: 'T : 'a + 'b iff ('T : 'a and 'T : 'b) i.e., ('T β‰₯ 'a and 'T β‰₯ 'b) iff 'T β‰₯ max('a, 'b)

    This is what makes the example that captured 'a and 'b not yield something that is dyn 'a + 'b + …, but rather, dyn 'a β‹‚ 'b + …, i.e., a dyn 'c + … where 'a : 'c, 'b : 'c:

    • /// Correct
      fn type_erasure<'a, 'b, 'c> (boxed: Box<(&'a (), &'b ())>)
        -> Box<dyn 'c + Object>
      where
          'a : 'c,
          'b : 'c,
      

6 Likes

@Yandros " .. And that's kind of the surprising part for Rust newcomers: this : 'static property is implicitly wired in their brain to hold for all types, since that's what happens in most other languages,..."

Thank you for empathizing with what refugees to Rust from other languages have to struggle with in Rust land. Rust can be a slow and confusing journey to those coming from the C/C++ world. This post is only one example of what many Rust newbies find confusing, I've also found Rust error handling online docs somewhat confusing as well, in that there are too many differing recommendations on how to do it.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.