Rust lifetimes: from 'static to ecstatic.

Foo

Case study: the pick() function

In these series we'll be dealing with kind of the lifetime-equivalent of the trolley problem: fn pick(&str, &str) -> &str and friends.

The body will always be along the lines of:


#![allow(unused)]
fn main() {
fn pick(left: &'_?? str, right: &'_?? str)
  -> &'_?? str
{
    if ::rand::random() {
        left
    } else {
        right
    }
}
}

And the whole question will be about what should we put instead of each '_??.

Let's try elision?

For starters, let's try the typical works-way-more-often-than-it-deserves do-no-overcomplicate approach of using elided lifetimes everywhere:

fn pick(left: &'_ str, right: &'_ str)
  -> &'_ str

❌ this fails because I wanted to make your life miserable the lifetime elision rules do not handle this situation: the inputs are so symmetric there is no clear "borrowee" argument.

Let's try repeating

Now we could also try using distinct lifetime parameters for all three, but that will "never" work: we return a borrowing thing, so it has to be borrowing from at least one of the arguments, thus, in practice, a borrowing return type almost always has to involve a lifetime parameter that appears among one of the input types.

Given how symmetric our code is, the sensible thing to do now would be to repeat all the lifetimes:

fn pick<'ret>(s1: &'ret str, s2: &'ret str)
  -> &'ret str

And this does:

  • compile ✅
  • not result in unnecesarily restrictive API ✅

Yay! 🥳

  • Aside: beginners often forget about the latter. Massaging a signature so that it becomes compatible with a given implementation often means we are making that function less compatible with certain call-site situations. While it can often be an acceptable loss, or a necessary one, other times that initial cargo check passing can hide that the API has become near-uncallable.

    That's why it is advisable to do this massaging of a function signature dance with an example usage / unit test next to it, so as to see the "two sides" of the caller/callee situation.

Congratulations, puzzle solved. Now to the next puzzle!

Consider:

use ::std::sync::{Arc, Mutex};

#[derive(Clone)]
struct Person<'name> {
    name: Arc<Mutex<&'name str>>,
}

fn pick(p1: Person<'_??>, p2: Person<'_??>)
  -> &'_?? str
{
    if ::rand::random() {
        *p1.name.lock().unwrap()
    } else {
        *p2.name.lock().unwrap()
    }
}

and with the following test case:

#[test]
fn test()
{
    let local = String::from("non-static");

    let p1: Person<'static> = Person {
        name: Arc::new(Mutex::new("static")),
    };
    let p2: Person<'_> = Person {
        name: Arc::new(Mutex::new(&local)),
    };
    let choice = pick(p1, p2);
    dbg!(choice);
}

Now, if you try the 'ret-everywhere approach, the test function will not compile!

error[E0597]: `local` does not live long enough
  --> src/lib.rs:49:35
   |
45 |     let p1: Person<'static> = Person {
   |             --------------- type annotation requires that `local` be borrowed for `'static`
...
49 |         name: Arc::new(Mutex::new(&local)),
   |                                   ^^^^^^ borrowed value does not live long enough
...
53 | }
   | - `local` dropped here while still borrowed

From the error message, we can guess that Rust is now convinced that our p2: Person<'_> is actually a Person<'static> (at which point it complains about &local not being able to be borrowed for 'static / 'forever due to the impending doom that looms over the local variable).

But why is that? Well, remember how the input of our pick() function was repeating the same 'ret lifetime parameter for both function arguments. Well, that's exactly what repeating a lifetime parameter means: lifetime equality! If p1 involves a 'static lifetime, then it means that p2's to-be-inferred lifetime has to be 'static. Hence the error.

  • For the skeptical ones who may still think the lifetime of the return value may also be playing a role here (which it is not), and for the otherwise just curious people, here is an interesting thing: since having both function args involving the same lifetime parameter was enough to cause this restriction, we can actually get rid of the return type altogether (and thus the function body as well), and still have the same problem! 😄

    fn pick<'ret>(_: Person<'ret>, _: Person<'ret>)
    // -> no return type whatsoever
    {
        // no meaningful body either 🙃
    }
    

Now, at this point we may wonder about the initial &str case, and see if it also suffers from this problem (had we been overly optimistic?)

fn pick<'ret>(s1: &'ret str, s2: &'ret str)
  -> &'ret str
{
    "…"
}

#[test]
fn test()
{
    let local = String::from("non-static");

    let s1: &'static str = "static";
    let s2: &'_ str = &local;
    let choice = pick(s1, s2);
    dbg!(choice);
}

Aaand… it does compile!

So, what has changed? Why/how does &'_ str work but Person<'_> not?

And the answer is that the '_ in &'_ str is "allowed to shrink", which is not the case for Person<'_>.

VARIANCE yadda yadda

Indeed, the main change here is that Person<'_>, contrary to &'_ str, does not allow its lifetime '_ to "shrink"; which is something the 'ret-everywhere approach was (probably unknowingly) relying on for the signature not to be restrictive. As

#[test]
fn test()
{
    let local1 = String::from("Jane");
    let local2 = String::from("Dove");

    let p1: Person<'_> = Person {
        name: Arc::new(Mutex::new(&local1)),
    };
    let p2: Person<'_> = Person {
        name: Arc::new(Mutex::new(&local2)),
    };
    let choice = pick(p1.clone(), p2.clone());
    dbg!(choice);
    if ::rand::random() {
        drop(p1); drop(local1);
        drop(p2); drop(local2);
    } else {
        drop(p2); drop(local2);
        drop(p1); drop(local1);
    }
}

Lifetime elision

The lifetime elision rules are just there as convenience sugar for what could otherwise be written in a more verbose manner. As with any sugar that does not want to become overly terse and footgunny (cough, C++, cough), it only makes sense to have it in clear unambiguous cases.

Lifetime elision in function bodies

Inside function bodies, that is, in a place where type inference is allowed, any elided lifetimes, implicit or not, will adjust to become whatever suits the code / whatever type-inference (with borrow-checking) dictates.

Lifetime elision in function signatures

By "function signatures" the following rules will apply to:

  • A function (item) signature:

    • fn some_func(…) -> …
      
  • A function pointer type:

    • type MyCb = fn(…) -> …;
      //          ^^^^^^^^^^
      
  • A Fn{,Mut,Once} trait bound:

    • : Fn{,Mut,Once}(…) -> …
      
    • impl Fn{,Mut,Once}(…) -> …
      
    • dyn Fn{,Mut,Once}(…) -> …
      

So, for all these, it turns out there are only two categories of "clear unambiguous cases":

  • When lifetimes play no meaningful role

    That is, when the function returns nothing that borrows or depends on inputs. That is, when the return type has no "lifetime" parameters:

    
    #![allow(unused)]
    fn main() {
    fn eq(s1: &'_ str, s2: &'_ str)
      -> bool // <- no "lifetimes"!
    }
    

    Since there isn't really any room for lifetimes/borrows subtleties here, the unsugaring will be a maximally flexible one. Given that repeating a lifetime parameter name is a restriction (wherein two lifetime-generic types will need to be using equal "lifetimes" / matching regions), we get to be maximaly flexible / lenient / loose by not doing that: by introducing and using distinct lifetime parameters for each lifetime placeholder:

    
    #![allow(unused)]
    fn main() {
    fn eq<'s1, 's2>(s1: &'s1 str, s2: &'s2 str)
      -> bool
    }
    
  • With borrowing getters

    With thus a clear unambiguous receiver / thing being borrowed:

    • 
      #![allow(unused)]
      fn main() {
      fn get(from_thing: &'_ Thing) // same for `BorrowingThing<'_>`.
        -> &'_ SomeField
      // replacing `&'_` with `&'_ mut` works too, of course.
      }
      
      1. In case of single "lifetime" placeholder among all the inputs;

      2. ⇒ the borrow of SomeField necessarily stems from it;

      3. ⇒ the output is to be "connected" to that input borrow by repeating the lifetime parameter.

        fn get<'ret> (from_thing: &'ret Thing)
          -> &'ret SomeField
        
    • 
      #![allow(unused)]
      fn main() {
      fn get(&'_ self, key: &'_ str)
        -> &'_ Value
      }
      
      1. A &[mut] self method receiver, despite the multiple lifetime placeholders among all the inputs, is "favored" and by default deemed to be the "thing being borrowed".

      2. ⇒ the borrow of the Value is to be "connected" to the self receiver; and key, on the other hand, is in a "I'm not borrowed" kind of situation.

      3. key will thus be using its own distinct "dummy" lifetime, whereas self and the returned Value will be using a single/repeated lifetime parameter.

        fn get<'ret, '_key> (
            self: &'ret Self,
            key: &'_key str,
        ) -> &'ret Value
        

Regarding fn-pointers and Fn… traits, the same rules apply, it's just that the named "generic" lifetime parameters for these things have to be introduced as "nested generics" / higher-rank lifetime parameters, using the for<> syntax.

  • In the fn-pointer case, we have:

    let eq: fn(s1: &'_ str, s2: &'_ str) -> bool;
    // stands for:
    let eq: for<'s1, 's2> fn(s1: &'s1 str, s2: &'s2 str) -> bool;
    
  • In the Fn… case, we have:

    impl/dyn Fn…(&'_ str, &'_ str) -> bool>
    // stands for
    impl/dyn for<'s1, 's2> Fn…(&'s1 str, &'s2 str) -> bool
    

So the rule of thumb is that "lifetime holes in ouput connect with an unambiguous lifetime hole in input".

  • Technically, lifetime holes can connect with named lifetimes, but mixing up named lifetimes with elided lifetimes for them to "connect" is asking for trouble/confusing code: it ought to be linted against, and I hope it will end up denied in a future edition:

    fn example<'a>(
        a: impl 'a + AsRef<str>,
        _debug_info: &'static str,
    ) -> impl '_ + AsRef<str>
    {
        a // Error, `a` does not live for `'static`
    }
    

Lifetime elision in dyn Traits

Yep, yet another set of rules distinct from that of elision in function signatures, and this time, for little gain 😔

  • tip: if you're the one writing the Rust code, avoid using these to reduce the cognitive burden of having to remember this extra set of rules!

This is a special form of implicit elision: dyn Traits1. Indeed, behind a dyn necessarily lurks a "lifetime"/region: that of owned-usability.

dyn Traits // + 'usability?
  • ⚠️ this is different than the explicitly elided lifetime, '_.

    That is, if you see a dyn '_ + Trait it will be the rules for lifetime elision in function signatures which shall govern what '_ means:

    fn a(it: Box<dyn Send>) // :impl Fn(Box<dyn 'static + Send>)
    
    fn b(it: Box<dyn '_ + Send>) // :impl for<'u> Fn(Box<dyn 'u + Send>)
    
1

When I write Traits, it is expected to cover situations such as Trait1 + Trait2 + … + TraitN

Rules of thumb for elision behind dyn:

  1. Inside of function bodies: type inference.

  2. &'r [mut] dyn Traits = &'r [mut] (dyn 'r + Traits)

    • More generally, given TyPath<dyn Traits> where TyPath<T : 'bound>, we'll have TyPath<dyn Traits> = TyPath<dyn 'bound + Traits>.
  3. otherwise dyn Traits = dyn 'static + Traits

Lifetime elision in impl Traits

Return-position -> impl Traits (RPIT)

Similarly to dyn, here, -> impl Traits ≠ -> impl '_ + Traits.

But contrary to other properties/aspects of type erasure (where dyn and -> impl can be quite similar), when dealing with lifetimes / captured generics, impl Traits happens to involve different semantics than those of dyn Traits ⚠️

See the dedicated section for more info.

Argument-position impl Traits (APIT)

  • (dubbed "universal" / caller-chosen)

Intuition: impl Traits will "be the same as impl '_ + Traits", that is, introducing a new named generic lifetime parameter <'usability>, and then impl 'usability + Traits. This aligns with the idea of it being a "universal" impl type.

Rationale / actual semantics

These are kind of analogous to the following:

For each impl Traits occurrence,

  1. introducing a new generic type parameter: <T>
  2. bounded by the Traits: where T : Traits
  3. and replacing the impl Traits with that parameter: T.
fn example(a: impl Send, b: impl Send)
// is the "same" as:
fn example<A, B>(a: A, b: B)
where
    A : Send,
    B : Send,
  • the only difference will be that in the latter signature, callers will be able to use turbofish to specify the actual choices of A or B, whereas in the former case this will be left for type inference.

Variance

Covariance: when "lifetimes" can shrink

So, in some of the previous chapters we've seen that sometimes, there is a seemingly silly operation which can be done: that of shrinking or shortening expiry-dates / regions / "lifetimes".

For instance, let's consider the simplest lifetime-infected type: a borrow (to, say, some primitive type):

&'borrow i32
// but also:
&'borrow mut i32

When you got your hands on such a thing with a 'long borrow, if you have to feed it to some API that wants a 'short borrow instead, you can!

use ::core::convert::identity;

fn demo<'short, 'long : 'short>(
    r: &'long mut i32,
) -> &'short mut i32
{
    identity::<&'short mut i32>(r) // ✅ OK!
}

This is called a subtyping relation, and you may observe that it is quite similar to an implicit coercion.

I'll personally be using the following syntax to express this subtyping property:

&'long mut i32 ➘ &'short mut i32
  • In the type-theory realm, they instead use <: for this, but I've never been very fond of this sigil, especially given how for lifetimes it's when we have 'big ≥ 'short that we have &'big () <: &'short (), which I find more confusing than saying &'big () ➘ &'short ().

    I like because:

    • it looks like a -> without being exactly that one (so as to avoid ambiguities with function signatures);
    • we could almost interpret it as a "decay"ing operation, which I personally find to fit quite well the subtyping properties within actual code 🙂 (the amount of type information goes down as we fit into a supertype).

Is shrinking lifetimes really "silly"?

Click to see this section

I guess the main example would be when trying to put together references with distinct lifetimes into a single collection such as a Vec:

let local = String::from("…");
let names: Vec<&str> = vec![local.as_str(), "static str"];

Or, similarly, when type-unifying the output of two branches:

let storage: PathBuf;
let file_to_read: &Path =
    match ::std::env::args_os().nth(1) {
        | Some(s) => {
            storage = s.into();
            &storage
        },
        // no `.to_owned()` whatsoever needed in this branch 💪
        | None => "default-file.toml".as_ref(),
    }
;

Finally, remember the pick() function section:

fn pick<'ret>(
    left: &'ret str,
    right: &'ret str,
) -> &'ret str
{
    if … { left } else { right }
}

Imagine trying to call pick() on a &String::from("…") and a "static str": with this signature, the borrows are constrained to have equal lifetimes! Lifetime shrinkage to the rescue 🙂

Even in the more pedantic case of doing:

fn pick<'ret, 'left, 'right>(
    left: &'left str,
    right: &'right str,
) -> &'ret str
where
    'left : 'ret,
    'right : 'ret,
{
    // Here, we have `'left` and `'right` lifetimes in  branch;
    // by covariance they get to shrink-unify down to some `'ret` intersection.
    if … { left } else { right }
}

notice how the left and right expressions in each branch of the if, respectively, by allowing the lifetime within each to shrink, managed to unify under a single shrunk lifetime, 'ret (cough intersection cough).

Covariance, a definition

Covariant lifetime parameter

So, this property of certain generic lifetime parameters being allowed to shrink is called covariance (in that lifetime parameter). While most generic types will happen to be covariant, there is an important exception / counter-example which we need to be aware of:

  • mutable…
  • …borrows!

When both properties are "ticked"/met, then any lifetime in the borrowee is not (allowed) to be shrunk. We say of such a lifetime that it is non-covariant, or for short1, invariant.

  • ⚠️ notice how we're talking of the lifetimes that may occur within the borrowee: the lifetime of the borrow itself is not part of the invariant stuff.

For instance, let's look at the type of that second argument of {Display,Debug}::fmt function:

&'_ mut Formatter<'_>

Naming the lifetimes, we end up with:

// mutable borrow of
//  vvvvvvvvvvvv
    &'borrow mut Formatter<'fmt>
//               ^^^^^^^^^^^^^^^
//              invariant borrowee



//  can shrink
//      =
//  covariant
//   vvvvvvv
    &'borrow mut Formatter<'fmt>
//                         ^^^^
//                     cannot shrink (nor grow)
//                           =
//                       invariant

A more typical albeit potentially visually confusing example is when the borrowee is itself a borrow:

&'borrow mut (&'fmt str)

In both cases, we have a &mut-able 'borrow, with thus a covariant/shrinkable lifetime, and behind such borrow, a thus invariant borrowee, resulting in 'fmt not being allowed to shrink (nor grow).

  • Reminder that shared mutability exists

    Note that the unique borrow, &mut, is not the only form of mutable borrow. Consider, for instance:

    // Covariant in `'borrow`, invariant in `'fmt`.
    &'borrow Cell<&'fmt str>
    
    • To gain a better intuition of this, I highly recommend that you open your mind to the following "statement":

      use ::core::cell::Cell as Mut;
      
      • (this is a legitimate thing to do since Cell does indeed let you mutate "its interior")

      Thay way we end up with:

      &'borrow Mut<&'fmt str>
      

    Finally, for technical reasons2, it turns out that the harmless owned shared mutability wrappers have to be declared invariant as well, despite the lack of borrows:

    // Invariant too!
    Cell<&'_ str>
    

    In practice, you're more likely to run into this when dealing with things like a Mut ex, a RwLock<_>, or any form of channel (at least the Sender<_> end):

    //! Invariant
    Mut ex<&'_ str>
    RwLock<&'_ str>
    RefCell<&'_ str>
    channel::Sender<&'_ str>
    

Covariant type parameter and composition rules

It turns out that saying:

  • given &'borrow /* Borrowee */,
  • then, each lifetime occurring in Borrowee (if any),
  • is covariant;

is kind of a mouthful to say/write/express. We thus define this conventional term, when this occurs: we'll talk of &'borrow T being covariant in the T type parameter.

So a not-so-formal definition of type-parameter-covariance would be:

  • Determining whether a <T>-generic type is covariant

    A practical rule (of thumb) for determining whether a <T>-generic type is covariant (in T) is to replace T by, say. &'s str, and seeing the covariance (in 's) of the resulting type, since that will then be the covariance of the <T>-generic type.

    Examples:

    • What is the covariance of type Example<T> = (T, i32);?
      1. We replace T with &'s str: what is the covariance of (&'s str, i32)?

      2. We somehow know that type ExampleT<'s> = (&'s str, i32) is covariant (in 's).

      3. "Thus", Example<T> is covariant (in T).

    • What is the covariance of type Example<T> = Arc<Cell<T>>;?
      1. We replace T with &'s str: what is the covariance of Arc<Cell<&'s str>>?

      2. We know that "lifetimes behind a Cell are invariant", so we know that:

        type ExampleT<'s> = Arc<Cell<&'s str>> is invariant (in 's).

      3. "Thus", Example<T> is invariant (in T).

  • Composition rules: when you know that a <T>-generic type is covariant:

    For instance, consider type Vec<T> (or, more generally, any type F<T>).

    Now define some type depending on a lifetime parameter:

    type T<'lt> = (i32, &'lt str, bool); // FYI, this is covariant.
    

    Then the resulting type, F<T<'lt>>, i.e., Vec<(i32, &'lt str, bool)>, will have the same variance as T<'lt> alone: <T>-covariant types let the <'lt>-variance inside T pass through.

1

when also non-contravariant.

2

if Cell<T>, owned, were covariant in T, since &C is covariant in C, it would mean that &Cell<T>. Which can't be. So Cell<T> can't be covariant in T.

Until now I've been talking of covariant / shrinkable lifetimes, or lack thereof (non-covariant / unshrinkable lifetimes).

But it turns out that, as surprising as it may initially seem, there are some specific types for which the lifetime parameter, rather than shrink, can actually grow.

Contravariance: when lifetimes can grow

The only time a lifetime is allowed to grow is when it occurs in function argument position:

  1. type MyCb = fn(String);
    

    which we have to make generic (reminder: variance is a property of generic types exclusively):

  2. type MyGenericCb<Arg> = fn(Arg);
    // or:
    type MyLtGenericCb<'lt> = fn(&'lt str);
    

In this case, we'll say that MyGenericCb<Arg> is contravariant in Arg ⇔ Lifetimes occurring in Arg are allowed to grow ⇔ 'lt is allowed to grow in MyGenericCb<&'lt str> = fn(&'lt str) = MyLtGenericCb<'lt>MyLtGenericCb is contravariant.

Growing lifetimes???

To get an intuition as to why/how can this case of growing lifetimes be fine, consider:

type Egg<'expiry> = &'expiry str; // or smth else covariant.
struct Basket<'expiry>(Vec<Egg<'expiry>>);

impl<'expiry> Basket<'expiry> {
    fn stuff(
        egg: Egg<'expiry>,
    )
    {
        let mut basket: Self = Basket::<'expiry>(vec![egg]);
        /* things with basket: Self */
        drop(basket);
    }
}

Now, imagine wanting to work with a Basket<'next_week>, but only having an Egg<'next_month> to construct it:

fn is_this_fine<'next_week, 'next_month : 'next_week>(
    egg: Egg<'next_month>,
)
{
    let stuff: fn(Egg<'next_week>) = <Basket<'next_week>>::stuff;
    stuff(egg) // <- is this fine?
}

We have two dual but equivalent points of view that make this right:

  1. Egg<'expiry> is covariant in 'expiry, so we can shrink the lifetime inside egg when feeding it: "we can shrink the lifetimes of covariant arguments right before they are fed to the function";

  2. at that point "we can directly make the function itself take arguments with bigger lifetimes directly"

    That is:

    1. Given some 'expiry lifetime in scope (e.g., at the impl level):

      fn stuff(
          egg: Egg<'expiry>,
      )
      
    2. We could always shim-wrap it:

      fn cooler_stuff<'actual_expiry>(
          egg: Egg<'actual_expiry>,
      )
      where
          //             ≥
          'actual_expiry : 'expiry,
      {
          Self::stuff(
              // since `Egg<'actual_expiry> ➘ Egg<'expiry>`.
              egg // : Egg<'expiry>
          )
      }
      
    3. So at that point we may as well let the language do that (let stuff subtype cooler_stuff):

    // until_next_month ⊇   until_next_week
    //    'next_month   :        'next_week
    fn(Egg<'next_week>) ➘ fn(Egg<'next_month>)
    

That is:

fn(Egg<'short>) ➘ fn(Egg<'long>)

Composition rules for a contravariant type-generic type

The gist of it is that a type-generic contravariant type, such as:

type MyGenericCb<Arg> = fn(Arg);

will flip the variance of the stuff written as Arg.

  • For instance, consider:

    type Example<'lt> = fn(bool, fn(u8, &'lt str));
    
    • Don't try to imagine this type used in legitimate Rust or you may sprain your brain 🤕

    That is, MyGenericCb<Arg<'lt>> where type Arg<'lt> = fn(u8, &'lt str);.

    1. type Arg<'lt> = fn(u8, &'lt str); is contravariant;
    2. type MyGenericCb<Arg> = fn(bool, Arg) is contravariant, so it flips the variance of the inner Arg<'lt>
    3. That is, MyGenericCb<Arg<'lt>> is "contra-contravariant", i.e.,
      type Example<'lt> = fn(bool, fn(u8, &'lt str))
                        = MyGenericCb<Arg<'lt>>
      
      is covariant.

Variance rules: a recap

Definition

Variance is a property of generic types:

  • Variance for a lifetime-generic type (e.g., Formatter<'_>) will determine whether lifetimes can:

    • shrink (covariant),
    • grow (contravariant),
    • or neither (invariant).
  • Variance for a type-generic type (Vec<T>) will determine:

    • intuitively, if that type parameter (T) were replaced with a lifetime-generic type itself (type T<'r> = &'r str), how the variance of that inner T<'r> type will propagate and affect the resulting variance of the composed type (Vec<T<'r>>).

      ×T<'lt>
      Co-variant
      T<'lt>
      Contra-variant
      T<'lt>
      In-variant
      F<T> covariant
      =
      "passthrough"
      Covariant
      F<T<'lt>>
      Contravariant
      F<T<'lt>>
      Invariant
      F<T<'lt>>
      F<T> contravariant

      =
      "flips it around"
      Contravariant
      F<T<'lt>>
      Covariant
      F<T<'lt>>
      Invariant
      F<T<'lt>>
      F<T> invariantInvariant
      F<T<'lt>>
      Invariant
      F<T<'lt>>
      Invariant
      F<T<'lt>>

      Tip: if you see covariance as "being positive" (+), and contravariance as "being negative" (-), and invariance as "being zero" (0), these composition rules are the same as the sign multiplication rules!

      ×+1-10
      +1+1-10
      -1-1+10
      0000
    • formally, how the subtyping relation among two choices for the type parameter results in some subtyping relation for the generic type.

      That is, if you have T ➘ U (T a subtype of U), and type F<X>;

      • if F is covariant, then F<T> ➘ F<U>;
      • if F is contravariant, thenF<U> ➘ F<T> (reversed!);

      You can have T ➘ U by:

      • common case: having a variant type T<'lt>, and picking the lifetimes accordingly (this is the intuitively section just above);

      • niche case: having a higher-rank fn pointer type

        and certain concrete choices of the inner generic lifetime:

        //! Pseudo-code:
        //! using `fn<'any>(…)` instead of `for<'any> fn(…)`
        
        // From higher-order lifetime to a fixed choice:
        fn<'any>(&'any str) ➘ fn(&'fixed str)
        
        // From a covariant lifetime to a higher-order one:
        fn(fn(&'static str)) ➘ fn<'any>(fn(&'any str))
        

        Interestingly enough, it does mean that if we pick:

        • type T = for<'any> fn(fn(&'any str));
          type U = fn(fn(&'static str));
          

        Then:

        • T ≠ U;

        • T : 'static and U : 'static;

        • T ➘ U by "fixing the higher-order lifetime"

          ⚠️ A : 'static bound is not sufficient to prevent subtyping shenanigans form happening! ⚠️

        • U ➘ T by "higher-ordering" a covariant lifetime by "induction from 'static".

        • Playground

Main variance examples to keep in mind

  • Mutable borrows and shared mutability wrappers are invariant.
    • Mutex<T>, Cell<T>, &'cov Cell<T>, &'cov mut T are invariant (in T);
  • otherwise, generally, owned stuff or immutably-borrowed stuff can be covariant.
    • T, &'_ T, Box<T>, Arc<T>, are covariant.
  • fn(CbArg) is contravariant in CbArg.
    • But fn(…) -> Ret is covariant in Ret!
  • impl Traits<'lt> and dyn Traits<'lt> are invariant;
    • this includes the Fn{,Mut,Once} traits.
  • + 'usability is covariant:
    • Bonus: there is also a no-overhead-upcasting/reünsizing coercion possible from &mut (dyn 'big + Traits) to &mut (dyn 'short + Traits), which to the untrained eye could appear as if &mut (dyn 'lt + Traits) were covariant in 'lt, which it is not.

Variance of "product types" / structural records / tuples

The rule of thumb is: combine them restrictively / non-covariance and non-contravariance are infectious.

By that I mean that you could think of variance as of marker traits:

  • trait Covariant {}
  • trait Contravariant {}
struct Example<'lt> {
    x: &'lt str,            //  Covariant + !Contravariant
    y: Mutex<&'lt bool>,    // !Covariant + !Contravariant
}                           // !Covariant + !Contravariant = Invariant

For instance, in this Example, we have a non-contravariant field alongside an invariant, i.e., neither-covariant-nor-contravariant field.

Thus, the resulting Example can't be:

  • contravariant, due to either field;
  • covariant, due to the second field.

So we have a neither-covariant-nor-contravariant type, i.e., an invariant one.

Lifetime semantics of -> impl Trait

-> impl Trait automagically captures generic type parameters in scope

fn foo<'lt, T>(lt: &'lt (), ty: T)
  -> impl Sized

is sugar (and the only stable-Rust syntax at the moment) for:

#![feature(type_alias_impl_trait)]

fn foo<'lt, T>(lt: &'lt (), ty: T)
  -> __ImplTrait<T>
// where the compiler has secretly generated:
pub type __ImplTrait<T> = impl Sized; // no `'lt` generic param!
  • with __ImplTrait<T> being a compiler-generated struct, of sorts, which is generic over <T> and thus fully "infected by T".

That is, it has the semantics of:

//! Pseudo-code

fn foo<'lt, T>(lt: &'lt (), ty: T)
  -> impl Sized + InfectedBy<T> // no `+ InfectedBy<'lt>` !

What the helium is InfectedBy<T>?

It's pseudo-code of mine to express the property that this struct __ImplTrait<T> would have: for any region 'r, if the return type is usable within that region, then so must T itself be:

for<'r> (
    impl Sized + InfectedBy<T> : 'r
    ⇒
    T : 'r
)

Although the practical/intuitive aspect of it is rather the contra-position:

for<'r> (
    T : !'r // let's assume this is syntax to say that `T : 'r` does not hold.
    ⇒
    impl Sized + InfectedBy<T> : !'r
)

To illustrate, let's pick T = &'t str, for instance:

  • beyond 't, instances of type T = &'t str are not allowed to be used.
  • Then it means that beyond 't, the returned -> impl Sized won't be allowed to be used either (no matter the function body!).

These -> impl Sized + InfectedBy<T> semantics are what allow the returned instance to contain ty: T inside it.

But notice how, despite the 'lt generic "lifetime" parameter in scope, there is no + InfectedBy<'lt> in the unsugaring I've shown!

  • Example

    The following works fine:

    fn nothing_doër<'lt>(_: &'lt String)
      -> impl Fn()
    {
        || ()
    }
    
    let f = {
        let local = String::from("…");
        nothing_doër(&local)
    };
    f() // ✅ OK
    

    But the following does not:

    fn nothing_doër<T>(_: T)
      -> impl Fn()
    {
        || ()
    }
    
    let f = {
        let local = String::from("…");
        nothing_doër(&local)
    };
    f() // ❌ Error, `local` does not live long enough!
    

    😬


-> impl Trait does not implicitly capture generic "lifetime" parameters in scope

This is not an oversight of mine (nor of the language), but rather a deliberate choice of the design of -> impl Trait. See the associated RFC for more info about it, and the rationale behind that choice.

The fact there is no + InfectedBy<'lt> means the returned instance won't be allowed to contain lt: &'lt … inside it.

fn foo<'lt, T>(lt: &'lt (), ty: T)
  -> impl Sized
{
    lt // ❌ Error, return type captures lifetime `'lt`
       //    which does not appear in bounds.
}
Error message
error[E0700]: hidden type for `impl Sized` captures lifetime that does not appear in bounds
  --> src/lib.rs:11:5
   |
8  | fn foo<'lt, T>(lt: &'lt (), ty: T)
   |        --- hidden type `&'lt ()` captures the lifetime `'lt` as defined here
...
11 |     lt
   |     ^^
   |
help: to declare that `impl Sized` captures `'lt`, you can add an explicit `'lt` lifetime bound
   |
9  |   -> impl Sized + 'lt
   |                 +++++
  • By the way that suggestion is not right, since it is not fully generalizable. See below for an example.

The workaround, currently, is to make the lifetime appear in bounds. If your trait in question does not give you room for that, then you can still achieve that by virtue of defining a helper trait for this, which happens to map quite nicely to my previous InfectedBy<…> pseudo-code.

  1. Define a helper generic-over-a-lifetime trait:

    /// Dummy empty marker trait.
    pub trait InfectedBy<'__> {}
    impl<T : ?Sized> InfectedBy<'_> for T {}
    
  2. Add it (+ InfectedBy<…>) to the bounds of the returned -> impl Trait:

    //! Real rust code
    
    fn foo<'lt, T>(lt: &'lt (), ty: T)
      -> impl Sized + InfectedBy<'lt>
    {
        lt // ✅ OK
    }
    

FWIW, you can imagine that this procedure is easily automatable through a macro:

About + 'lt, a seemingly shorter, but not always applicable, workaround

In general, people will reach for a simple + 'lt kind of bound (rather than the more cumbersome + InfectedBy<'lt>).

But if you have understood the meaning of + 'usability correctly, you should realize how this only works for simple cases:

/// ✅
fn simple_case<'lt>(lt: &'lt ())
  -> impl 'lt + Sized
{
    lt
}

/// ❌
fn does_not_work<'a, 'b>(a: &'a (), b: &'b ())
  -> impl 'a + 'b + Sized
{
    (a, b)
}
Error message
error: lifetime may not live long enough
  --> src/lib.rs:21:5
   |
18 | fn does_not_work<'a, 'b>(a: &'a (), b: &'b ())
   |                  --  -- lifetime `'b` defined here
   |                  |
   |                  lifetime `'a` defined here
...
21 |     (a, b)
   |     ^^^^^^ function was supposed to return data with lifetime `'b` but it is returning data with lifetime `'a`
   |
   = help: consider adding the following bound: `'a: 'b`

error: lifetime may not live long enough
  --> src/lib.rs:21:5
   |
18 | fn does_not_work<'a, 'b>(a: &'a (), b: &'b ())
   |                  --  -- lifetime `'b` defined here
   |                  |
   |                  lifetime `'a` defined here
...
21 |     (a, b)
   |     ^^^^^^ function was supposed to return data with lifetime `'a` but it is returning data with lifetime `'b`
   |
   = help: consider adding the following bound: `'b: 'a`

help: `'a` and `'b` must be the same: replace one with the other

Indeed, + 'usability means that it has to be usable (at least) within the whole 'usability region!

So if you write + 'a + 'b it means that it will be usable within 'a and1 that it will be usable within 'b as well. That is, (an instance of (type)) impl 'a + 'b has to be usable within the union of the regions 'a and 'b.

1

that's exactly what + means in Rust: + adds capabilities, rather than restraining them, so there is no way + 'a + 'b yields, to the caller, something more restrictive than just + 'a.

But the (a, b): (&'a (), &'b ()) value we are returning is meant to be unusable whenever:

  • we leave the 'a region (because of its a component),
  • we leave the 'b region (because of its b component).

Which means (a, b) is only usable within the intersection of the 'a and 'b regions.

  • In expiry dates parlance, imagine a small bottle of milk, a, with an expiry date of one month, and another such bottle, b, but this time with an expiry date of one week. Say now that you mix the contents of both into a big bottle (this would be our (a, b) Rust item). Then the resulting mix should be drunk or thrown away within the week, lest the b part inside it go sour. That is, the resulting mix has an expiry date of 'b = min('a, 'b) = intersection('a, 'b).

    An impl 'a + 'b bottle of milk would, on the other hand, have meant that it would have been drinkable both before next week, and before next month, so effectively before next month ('a = max('a, 'b) = union('a, 'b)).

What we want to say is precisely that (a, b) is infected by both 'a and 'b, i.e., that it is InfectedBy<'a> + InfectedBy<'b>:

fn example<'a, 'b>(
    a: &'a str,
    b: &'b str,
) -> impl Fn(bool) + InfectedBy<'a> + InfectedBy<'b>
{
    move /* (a, b) */ |choice| {
        println!("{}", if choice { a } else { b })
    }
}

Granted, the previous function can be simplified:

fn example<'intersection_of_a_and_b>(
    a: &'intersection_of_a_and_b str,
    b: &'intersection_of_a_and_b str,
) -> impl 'intersection_of_a_and_b + Fn(bool)
  • since 'a and 'b, from the call-sites, would be able to shrink down to some 'intersection_of_a_and_b region smaller than both, which would bring us back to a single-lifetime scenario, and thus to using the convenient + 'usability syntax to get the returned existential to be infected by 'intersection_of_a_and_b.

But in more complex APIs, lifetimes may not be able to shrink, and a proper solution needs to be used if you are to make it work.

For instance, no matter how hard you try, there is no simpler signature for the following function:

fn impl_<'a, 'b>(
    a: Arc<Mutex<&'a str>>,
    b: Arc<Mutex<&'b str>>,
) -> impl Fn(bool) + InfectedBy<'a> + InfectedBy<'b>
{
    move /* (a, b) */ |choice| {
        println!("{}", if choice {
            *a.lock().unwrap()
        } else {
            *b.lock().unwrap()
        });
    }
}

Comparison to dyn Trait

Note that dyn Trait is actual type erasure, so if you manage to get your hands on the 'intersection lifetime of all the captured lifetime and type parameters, then you can simply use + 'intersection and Rust won't complain even when bigger unshrinkable lifetimes are part of the before-erasure type (in other words, type erasure is kind of able to shrink even otherwise unshrinkable lifetimes):

fn dyn_<'i, 'a : 'i, 'b : 'i>( // intersection('a, 'b) ⊇ 'i
    a: Arc<Mutex<&'a str>>,
    b: Arc<Mutex<&'b str>>,
) -> Box<dyn 'i + Fn(bool)>
{
    Box::new(move /* (a, b) */ |choice| {
        println!("{}", if choice {
            *a.lock().unwrap()
        } else {
            *b.lock().unwrap()
        });
    })
}

The keen eye may have spotted the "intersection lifetime" pattern. Otherwise, that newly introduced 'i "lifetime" parameter will probably seem weird or confusing, in which case you may want to check out the dedicated section.

Intersection lifetime

Imagine wanting to write:

//! Pseudo-code

// this is what `#[async_trait]` wants to write, for instance.
fn async_fn<'a, 'b>(
    a: Arc<Mutex<&'a str>>,
    b: Arc<Mutex<&'b str>>,
) -> BoxFuture<'a ^ 'b, i32>
//                👆
//           pseudo-code for `intersection('a, 'b)`

Alas, we can't write that. Indeed, 'a ^ 'b does not exist in current Rust (to keep things "simpler" I guess). So we have to resort to quantifications and set theory to express this property.

Behold, what peak "simpler" looks like:

Let A and B be two sets, and C, their intersection: C = A ∩ B

Then, by definition, A ⊇ C and B ⊇ C (C is a subset of A and C is a subset of B).

In fact, one can define the intersection with that property:

  1. Consider some set I which is a subset of both A and B:

    I where A ⊇ I and B ⊇ I

    • visually, you can observe that C ⊇ I.

      venn diagram
  2. Now imagine the maximum of all possible I / "try to make I bigger until you can't anymore": it's C!

A ∩ B = C = max { I where A ⊇ I and B ⊇ I }

Back to Rust, this becomes:

'a ^ 'b = max { 'i where 'a : 'i, 'b : 'i }

And two point of views are possible:

  • either we realize we don't necessarily need the max exactly, that something Big Enough™ would suffice. For instance, in our dyn example, we can realize that a subset of the actual intersection can suffice in practice (the subset where we actually use the dyn).

    In this case, we can focus on this freely introduced I and all will be good.

  • or we consider that a "free" extra generic parameter will be "min-maxed", as needed, by Rust's borrow checker, which is constantly trying to make our code pass/compile.

    With this more magical but simpler point of view, we just introduce I and let Rust auto-maximize it as needed into the actual full intersection of A and B: A ∩ B.

'a ^ 'b ≈ 'i where 'a : 'i, 'b : 'i

In both cases we end up with the following recipe for something like an intersection "lifetime"/region of regions 'r0, …, 'rN:

Recipe for 'i = 'r0 ^ … ^ 'rN

  1. <'i> (let there be I)

    That is, introduce 'i as a new/added "free" generic region parameter.

  2. Add

    where
        //  ⊇
        'r0 : 'i,
        'r1 : 'i,
        …
        'rN : 'i,
    

    as where clauses so as to guarantee that I is one of the { I where R0 ⊇ I and … and RN ⊇ I }.

  3. Use 'i as if it were the desired intersection lifetime.

  4. Profit™

Example

We had:

fn async_fn<'a, 'b>(
    a: Arc<Mutex<&'a str>>,
    b: Arc<Mutex<&'b str>>,
) -> BoxFuture<'a ^ 'b, i32>

We apply the recipe:

  1. //          👇
    fn async_fn<'i, 'a, 'b>(
    
  2. where
        'a : 'i,
        'b : 'i,
    
  3. - ) -> BoxFuture<'a ^ 'b, i32>
    + ) -> BoxFuture<'i, i32>
    

That is:

//          👇 (1)
fn async_fn<'i, 'a, 'b>(
    a: Arc<Mutex<&'a str>>,
    b: Arc<Mutex<&'b str>>,
//             👇 (3)
) -> BoxFuture<'i, i32>
where
    'a : 'i, // 👈 (2)
    'b : 'i, // 👈
  • (in the specific case of an async fn or Future, it may be more sensible to rename 'i as 'fut, or 'async_fn, or something along those lines)

async fn unsugaring / lifetime

Consider, for instance:

trait Trait {
    async fn async_fn<'a, 'b>(
        a: Arc<Mutex<&'a str>>,
        b: Arc<Mutex<&'b str>>,
    ) -> i32
    {
        dbg!(a.lock());
        dbg!(b.lock());
        42
    }
}

As of 1.66.1, in Rust, this cannot be written directly, since

async fn f(…) -> Ret

is sugar for

fn f(…) -> impl Future<Output = Ret>

and -> impl Trait cannot be used in traits yet.

In the case of Future, which is a dyn-able trait, and which is Pin<Box>-transitive (i.e., given T : ?Sized, if T : Future then Pin<Box<T>> : Future<Output = T::Output>), a workaround for this limitation is thus to write the function as -> Pin<Box<dyn Future…>>.

And since the process is:

  • tedious,
  • quite automatable,
  • and yet a bit subtle at times,

we end up with —you guessed it— macros to do this for us, such as #[async_trait]

But how does #[async_trait] do it?

  • Answering this question is more important than just intellectual curiosity. Indeed, what if:

To anticipate any of these cases, it's actually very sensible to learn to do what #[async_trait] does.

  1. So we start off

    trait Trait {
        async fn async_fn<'a, 'b>(
            a: Arc<Mutex<&'a str>>,
            b: Arc<Mutex<&'b str>>,
        ) -> i32
        {
            …
        }
    }
    
  2. We replace async fn … -> Ret with fn … -> impl Future<Output = Ret>:

    trait Trait {
        fn async_fn<'a, 'b>(
            a: Arc<Mutex<&'a str>>,
            b: Arc<Mutex<&'b str>>,
        ) -> impl Future<Output = i32>
                + InfectedBy<'a> + InfectedBy<'b>
        {
            async move {
                let _captures = (&a, &b);
                …
            }
        }
    }
    
  3. We Pin<Box>-wrap it:

    trait Trait {
        fn async_fn<'a, 'b>(
            a: Arc<Mutex<&'a str>>,
            b: Arc<Mutex<&'b str>>,
        ) -> Pin<Box<
                impl Future<Output = i32>
                   + InfectedBy<'a> + InfectedBy<'b>
            >>
        {
            Box::pin(async move {
                let _captures = (&a, &b);
                …
            })
        }
    }
    
  4. We let it dyn:

    ) -> Pin<Box<
    -       impl Future<Output = i32>
    +        dyn Future<Output = i32>
               + InfectedBy<'a> + InfectedBy<'b>
        >>
        {
            Box::pin(async move {
                let _captures = (&a, &b);
                …
            })
        }
    }
    
    • We won't bother with Send-ness, here.
  5. We can't really use + InfectedBy<'_> with dyn (and don't really need to, as we'll see below), so we get rid of that too:

          dyn Future<Output = i32>
    -       + InfectedBy<'a> + InfectedBy<'b>
    +       + '???
    

At this point we've ended up with the following return type:

dyn '??? + Future<Output = i32>

And now the million dogecoin question is to know which lifetime we put here:

  • dyn 'a + Future… ?
    • May cover a region outside that of 'b, so our future may dangle in there: ❌
  • dyn 'b + Future… ?
    • symmetrical situation: ❌
  • dyn 'a + 'b + Future… ?
    • Papering over the fact Rust doesn't let us write dyn 'a + 'b + … for some reason, this cannot be right, since this expresses a 'usability that includes 'a and 'b, and we've seen that each of these is already problematic, so a bigger usability will be just as problematic, if not more!

I've talked about all this in more detail over the section about -> impl Trait, which also happens to mention the answer.

The gist of the answer is that:

An impl InfectedBy<'a> + InfectedBy<'b> is only usable within the intersection of 'a and 'b.

InfectedBy<'a, 'b> : 'a ^ 'b

In other words, our '??? / the "'usability" of such an entity is 'a ^ 'b.

So we end up needing to write:

//! Pseudo-code

fn async_fn<'a, 'b>(
    a: Arc<Mutex<&'a str>>,
    b: Arc<Mutex<&'b str>>,
) -> BoxFuture<'a ^ 'b, i32>

This, as the non-existing-in-Rust 'a ^ 'b syntax suggests, is not real Rust code. See the intersection lifetime section to know more about how the above ends up becoming:

//! Real-code!

fn async_fn<'fut, 'a, 'b>(
    a: Arc<Mutex<&'a str>>,
    b: Arc<Mutex<&'b str>>,
) -> BoxFuture<'fut, i32>
where
    // ⊇
    'a : 'fut, 'b : 'fut,
 // 'a ^ 'b ⊇ 'fut

Appendix

(you're not really supposed to keep navigating linearly through the book at this point; what follows is just a series of standalone articles that the main parts of the book will refer to)

"Lifetime placeholders", or elided lifetimes

A lifetime can be elided:

  • either implicitly, as is the case for &str or &mut i32 (behind a & always lurks a lifetime parameter);
  • or explicitly, using the special '_ syntax.

Across this book, I personally (overly) use the latter syntax, so as to show, clearly, all the elided lifetime parameters.

The result of an elided lifetime is thus a lifetime "hole", or lifetime placeholder, which will be the key thing to look at when thinking about lifetime elision rules. For instance, all these types involve lifetime holes:

  • &i32, &'_ i32, &mut i32, &'_ mut i32,
  • GenericTy<'_>1
    • e.g., Cow<'_, str>, Formatter<'_>, Context<'_>, BoxFuture<'_, R>, BorrowedFd<'_>
  • dyn '_ + Trait, dyn GenericTrait<'_>, and ditto for impl …,
    • e.g., Pin<Box<dyn '_ + Send + Future<Output = R>>>
1

Historically, it has been possible in Rust to use a lifetime-generic type without the <'_>. But this can be very confusing, which is why since the edition 2018_idioms, there is a elided_lifetimes_in_paths lint which can be enabled (e.g., set to warn), to catch these things. It is highly advisable to always set up this lint in any Rust project.

See the lifetime elision rules for more info.

The meaning of + 'lifetime

Subtyping vs. Coercions

  • Example of subtyping:

    fn demo<'short>()
    {
        let s: &'static str = "…";
        let s: &'short str = s;
    }
    
  • Examples of coercions:

    fn implicit_coercions(a: &'_ mut Box<[u8; 4]>)
    {
        // `&mut` to `*mut` (& friends)
        let _: *mut _ = a /* as _ */;
    
        // `&mut` to `&` (this is not subtyping!)
        let b: &Box<[u8; 4]> = a /* as _ */;
    
        // `&impl Deref` to `&Deref::Output`
        let c: &[u8; 4] = b /* as _ */;
    
        // Unsized coercions:
        let _: &[u8] = c /* as _ */;
        let d: &dyn SubTrait = c /* as _ */;
        // `feature(trait_upcasting)`
        let _: &dyn SuperTrait = d /* as _ */;
    }
    // where:
    use ::core::any::Any as SuperTrait;
    trait SubTrait : SuperTrait {}
    

There are two differences between subtyping and implicit coercions:

  • Coercions are allowed to perform (basic) runtime operations to perform the conversion. For instance, the coercion from &[u8; 4] (thin data pointer to the beginning of 4 bytes) to &[u8] (attaching a runtime 4: usize "field" next to that thin data pointer to end up with a wide pointer).

    Whereas subtypining relations cannot, so as to have the following property:

  • Subtyping relations can be nested/chained within arbitrarily deep complex types (provided that they be covariant), whereas coercions can't:

    fn ok<'short, 'long : 'short>(v: Vec<&'long mut i32>)
      -> Vec<&'short mut i32>
    {
        v // ✅ OK
    }
    
    fn nope(v: Vec<&mut i32>)
      -> Vec<&i32>
    {
        v // ❌ Error
    }
    

The latter point, I'd say, is really the critical difference from a theoretical perspective, since, for instance, given some 'short lifetime, consider the following "generic type" / type constructor:

<T> => &'short T

It is a rather simple type, which happens to be covariant in T.

Covariance (using &'long X ➘ &'short X)

Let's try with type T = &'long String;, and trying to reach the type T = &'short String;, since the former subtypes the latter:

&'short (&'long String) ➘ &'short (&'short String)
// by covariance (in T) of `&'short T`
// since &'long String ➘ &'short String
// since 'long ⊇ 'short

Coercion? (using the &'long mut X ⇒ &'long X coercion)

Now let's try with type T = &'long mut String, and trying to go to type T = &'long String;, since we know the former can be coerced to the latter:

fn test<'short, 'long : 'short>(
    r: &'short (&'long mut String),
) -> &'short (&'long String)
{
    r /* as _ */
}

This errors! ❌

Error message
error[E0308]: mismatched types
 --> src/lib.rs:5:5
  |
3 | ) -> &'short (&'long String)
  |      ----------------------- expected `&'short &'long String` because of return type
4 | {
5 |     r /* as _ */
  |     ^ types differ in mutability
  |
  = note: expected reference `&'short &'long String`
             found reference `&'short &'long mut String`

Not only does it error, but there is actually no way to write this soundly.

Indeed, such operation is unsound, i.e., if it existed, it would make it possible, using it and non-unsafe Rust, to trigger UB/memory-corruption:

  1. Given:

    fn unsound<'short, 'long : 'short>(
        r: &'short (&'long mut String),
    ) -> &'short (&'long String)
    {
        unsafe { ::core::mem::transmute(r) }
    }
    
  2. One can write:

    /// Here is an example of what `unsound` lets us do:
    fn exploit_helper<'long>(
        s: &'long mut String,
    ) -> (
            &'long String,
            &'long mut String,
        )
    {
        // `s` cannot be used while `short` (or data
        // derived from it) is being used.
        let short: &'_ &'long mut String = &s;
        // the `'short` lifetime in `changed` is still
        // derived from `short`, so `s` still can't be used
        let changed: &'_ &'long String = unsound(short);
        // But we can now dereference `changed`,
        // since `&'long String : Copy`,
        // and the resulting `&'long String` is no longer
        // tied to `short`.
        let r: &'long String = *changed;
        // That is, we can freely use `s` even if `r` is used too!
        (r, s)
    }
    

    Which means we'd have a (shared) reference aliasing with an exclusive one! UB

  3. For instance:

    let s = &mut String::from("Hey!");
    let (r, s) = exploit_helper(s);
    // Have `r` point to the heap-allocated utf-8 bytes the `String` owns.
    let r: &str = &*r;
    // And now let's deallocate those bytes:
    *s = String::new();
    // read deallocated memory:
    dbg!(r);
    
    error: Undefined Behavior: trying to retag from <3360> for SharedReadOnly permission at alloc1685[0x0], but that tag does not exist in the borrow stack for this location
      --> src/main.rs:19:5
       |
    19 |     (r2, r)
       |     ^^^^^^^
       |     |
       |     trying to retag from <3360> for SharedReadOnly permission at alloc1685[0x0], but that tag does not exist in the borrow stack for this location
       |     this error occurs as part of retag at alloc1685[0x0..0x18]
       |
       = help: this indicates a potential bug in the program: it performed an invalid operation, but the Stacked Borrows rules it violated are still experimental
       = help: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/stacked-borrows.md for further information
    help: <3360> was created by a SharedReadOnly retag at offsets [0x0..0x18]
      --> src/main.rs:19:6
       |
    19 |     (r2, r)
       |      ^^
    help: <3360> was later invalidated at offsets [0x0..0x18] by a Unique retag
      --> src/main.rs:19:10
       |
    19 |     (r2, r)
       |          ^
       = note: BACKTRACE (of the first span):
       = note: inside `exploit_helper` at src/main.rs:19:5: 19:12
    note: inside `main`
      --> src/main.rs:25:18
       |
    25 |     let (r, s) = exploit_helper(s);
       |                  ^^^^^^^^^^^^^^^^^