What is the exact meaing of the restriction brought by a lifetime annotation

I have some confusion for the example in nomicon regarding lifetime

#[derive(Debug)]
struct Foo;

impl Foo {
    fn mutate_and_share(&mut self) -> &Self { &*self }
    fn share(&self) {}
}

fn main() {
    let mut foo = Foo;
    let loan = foo.mutate_and_share();
    foo.share();
    println!("{:?}", loan);
}

This example is error. The explanation in that book is as the following

struct Foo;

impl Foo {
    fn mutate_and_share<'a>(&'a mut self) -> &'a Self { &'a *self }
    fn share<'a>(&'a self) {}
}

fn main() {
    'b: {
        let mut foo: Foo = Foo;
        'c: {
            let loan: &'c Foo = Foo::mutate_and_share::<'c>(&'c mut foo);
            'd: {
                Foo::share::<'d>(&'d foo);
            }
            println!("{:?}", loan);
        }
    }
}

The book says

The lifetime system is forced to extend the &mut foo to have lifetime 'c, due to the lifetime of loan and mutate_and_share's signature.

However, IMO, doesn't the annoation in &'c mut foo mean the borrowed value foo should outlive 'c? Why the book say the lifetime system is forced to extend the reference(borrow) &mut foo to have lifetime 'c?

It also means that foo is borrowed (or, as some might say, locked) mutably (and therefore exclusively) for the region 'c.

There have been a lot of lifetime threads lately, and I can't recall who I pointed where, so forgive me if I'm repeating myself to you.


This portion of the Nomicon is addressing a common expectation people learning Rust have, which is that after calling a method that looks like so...

fn exclusive_borrow_shared_return(&mut self) -> &Thing { /* ... */ }

...self will no longer be mutably borrowed, because the returned value is a shared borrow.

However, that's not how the lifetimes work. In desugared form, the signature is:

fn exclusive_borrow_shared_return<'a>(&'a mut self) -> &'a Thing

And the most simple way I can describe it is that "self must be exclusively borrowed for the same duration as the returned &'a Thing is valid." [1]

For the behavior to match the intuition, it would have to be some sort of two-phased borrow -- two distinct borrows in succession. A brief exclusive borrow of self for the duration of the method call, immediately and seamlessly followed by a shared borrow of both self and the returned value.

As noted in this article, we can't change the existing rules to work like that, as the existing rules are relied upon for soundness. It would have to be some new feature of the language, which we don't have so far.


Let's do a thought experiment within the current Rust rules, anyway. The experiment is: can we mutably borrow for a short period and return a shared value that is valid for a longer period? Here's what such a method might look like, from a lifetime declaration standpoint:

    fn short_to_long<'long: 'short, 'short>(&'short self) -> &'long i32 {
        todo!()
    }

It compiles as-is. But if we actually try to return a reference to a field here, we cannot:

error: lifetime may not live long enough
 --> src/lib.rs:7:9
  |
6 |     fn short_to_long<'long: 'short, 'short>(&'short self) -> &'long i32 {
  |                      -----          ------ lifetime `'short` defined here
  |                      |
  |                      lifetime `'long` defined here
7 |         &self.field
  |         ^^^^^^^^^^^ associated function was supposed to return data with lifetime `'long` but it is returning data with lifetime `'short`
  |
  = help: consider adding the following bound: `'short: 'long`

If we followed the advice, we would have both

'long: 'short, // 'long is valid for at least 'short
'short: 'long  // 'short is valid for at least 'long

And that means the lifetimes would have to be equal, and we're back to the signature where the returned lifetime and the input lifetime are the same.

You can reborrow a reference for some shorter duration, but you can not reborrow a reference for some longer duration. In order to create the borrow of the field for 'x, you yourself had to have access for at least 'x.

When exactly you can "shrink" lifetimes or not, or even grow them in some cases, is determined by a property called variance. But a good starting point is, "you can't reborrow something for longer than you borrowed it." If I rent a house for a week, I can't let someone else use the bedroom for a month.

You can't borrow self for 'short and then hand out a borrow of some field for 'long, either.


  1. I had to work through various mental models of lifetime transfers and reborrowing, etc, until I finally realized that the rule can be very simply understood syntactically. The lifetime of the returned value and the lifetime of the exclusive borrow of self must be the same, and that is that. You must create the exclusive borrow of the same duration in order to call the method. The exclusiveness or not of the returned type does not matter; in order to get the returned value, you have to call the method; in order to call the method, you have to create that exclusive borrow. ↩ī¸Ž

6 Likes

In simple, since the loan should be valid from the point where its binding is completed to the point where it is used to print, such a region(named C) is what the lifetime parameter 'c should denote, hence the reference &'c mut foo should be alive in region C. Simultaneously, there exists an immutable borrow(&' d foo), which is within the region C, hence they violate the reference rules. Is this a correct understanding?

Yes.

Being pedantic: any overlap of 'd and 'c is a problem because 'c is an exclusive borrow ('d doesn't have to be strictly within 'c).

I can't remember if I gave you or two other people this link, but perhaps give it a read if you haven't already. I may diagram the borrows later if that would help.

Here's a diagram in the style of those other posts / stacked-borrow-esque.

2 Likes

There seems to be a lifetime visualization tool for that case. But I never use it.

2 Likes

Oh, nice! I will have to check that out. I've wanted such a thing for approximately :infinity:.

2 Likes

IIUC, that example is equivalent to this one

struct Foo;
fn main(){
   let mut foo = Foo;  // #1
   let mrf = & mut foo;  // #2
   let rf = &foo;  // #3
   mrf; // #4
}

Even though the immutable borrow produced by &foo only has a very short lifetime that is almost expired at point #3, however, the immutable has overlapped with the lifetime of & mut Foo, which is just not permitted in rust. Is it?

I think I have got the correct conclusions from your answers:

  1. lifetime is designed for reference/borrow, which indicates where region the borrow/reference can be valid, where the region is a range and the range guarantees that the use of the borrow/reference is valid as long as the use is within the range(i.e. not outside the range).

  2. The lifetime in reference type does not directly apply to the referenced value/content, however, each time we create a borrow/reference &'a T, the implicit condition should be satisfied, that is T: 'a. If T is not a reference type or does not contain reference fields, then T always satisfies T:'static therefore it also satisfies T:'a. Otherwise, the condition should be satisfied for each reference that is or contained by T, which is recursive.

Is my understanding right?

The pattern is the same, yes.

Correct, any access of foo conflicts with an exclusive borrow, no matter how short. Once the shared borrow at #3 happens, mrf no longer has exclusive access. So you can't use it at #4.

  • (1) Your understanding seems pretty much correct.

  • (2) Your understanding seems pretty much correct.

    • While the root source of lifetimes is generally always a reference, there can technically not be any references involved with T itself, so a more correct phrasing may be something like "If T does not have a non-'static lifetime parameter".
    • E.g., PhantomData
    • E.g., Every dyn Trait has a lifetime of applicability
      • The mention of references in that documentation is again technically too specific; the lifetime of a trait object specifies where it is valid, regardless of the presence of a reference or not.
5 Likes

@quinedot Could you please interpret this example

fn test_lifetime<T:'static>(v:T){
    unimplemented!()
}

fn main(){
  static I:i32 = 0;
  let rf = &I;
  test_lifetime(rf);
}

In this example, If we try to illustrate the lifetime of rf, we will get the following diagram

'static:{
   let I = 0;
   fn main(){
     'a:{
      let rf:&'a i32 = &'static I;  // covariant occurs here
      test_lifetime(rf); // the trait bound requires `&'a i32` to satisfy 'static
     }
  }
}

As we discussed in above, T: 'static means the reference should outlive 'static(if T is a reference). Obviously, in this example, 'a lives shorter than 'static. However, this example just works.

This should be let rf:&'static i32 = &'static I;, which is called static promotion.

Edit: Well, it just keeps the reference static from a static item.

All references to the static refer to the same memory location. Static items have the static lifetime, which outlives all other lifetimes in a Rust program. -- the Reference

1 Like

I think you're conflating the liveness scope of the variable rf with the lifetime which is part of its type. Just as a String variable need not last the entire duration of a program even though String: 'static, you can have a local variable with the type &'static i32.

The most common example is probably holding a &'static str ("...").

That's what's happening here. It's okay to take a static reference to a static variable, no matter where you store the reference.

(Literals can also be promoted to statics automatically, like with literal &strs.)

2 Likes

(This isn't promotion as the underlying value is already a static.)

But the link explains why this works with &0, which is useful to understand. Or with "...".

Thanks for pointing that out!

So, does it mean, if we didn't explicitly specify the lifetime for the reference, the reference would have the maximum lifetime as it can? In this example, since the reference/borrow is taken from I which has a static lifetime, hence, the binding of the rf is as if it was

 let rf:&'static i32 = &'static I; 

If we modify the example to that

fn shrink_the_lifetime<'a>(original_static:&'a i32, used_for_shrink:&'a i32){}
fn main(){
   static I:i32 = 0;
   let rf;
   {
      rf = &I;
      let ii = 0;
      let rf_block = &ii;
      shrink_the_lifetime(rf, rf_block); // shrink occurs? 
   }
   rf;
}

@quinedot However, as pointed out in the question, "nomicon" does use shorter lifetime to annotate the block variable, such as

fn main() {
    'b: {
        let mut foo: Foo = Foo;
        'c: {
            let loan: &'c Foo = Foo::mutate_and_share::<'c>(&'c mut foo);
            'd: {
                Foo::share::<'d>(&'d foo);
            }
            println!("{:?}", loan);
        }
    }
}

loan's liveness scope is within 'c, the reference/borrow taken from foo will have lifetime 'c (&'c Foo), even though foo has a lifetime 'b that is longer than 'c.

AFAIK, the lifetime annotation doesn't shrink lifetime. It's more like a contract or bound that checks whether your code meets the requirements.

{
      rf = &I;
      let ii = 0;
      let rf_block = &ii; // &'1 i32
      shrink_the_lifetime(rf, rf_block); // rf: &'static i32 can shorten as &'1 i32 via covariance, so the bound is satisfied and your code passes
}
1 Like

No. Generally inferred reference lifetimes are as short as possible as that is maximally flexible. Though

  • I don't know that this is technically guaranteed
  • "a particular lifetime is assigned" is probably less accurate than "no lifetime conflicts were detected"

In this case, 'static was the shortest (and only and thus incidentally longest) lifetime possible to infer and still complie.

I'll probably provide some links in a bit (on mobile rn).

1 Like

What do you think about this question, What is the exact meaing of the restriction brought by a lifetime annotation - #16 by xmh0511.

The foo has lifetime 'b but the reference/borrow taken from it instead has a lifetime 'c , which is shorter than 'b. In contrast, I has a lifetime 'static but the reference/borrow taken from it has the same lifetime 'static? Which is my confusion here.