Is this right model for understanding lifetimes

I learned (maybe) bit about lifetimes of course with help of others and my own
Is this right model to have
Consider this function

fn aaa<'a, 'b>(arg: &'a mut [u8;2]) -> (&'a mut [u8], &'a mut [u8]) {
    let a = unsafe {arg.split_at_mut_unchecked(1)};
    (a.0, unsafe {core::slice::from_raw_parts_mut(a.1 as *mut [u8] as *mut u8 as usize as *mut u8, 1)})
}

This function has unbounded lifetime so applying with both parameters ‘a

fn main() {
    let mut a = [6u8; 2]; 
    let (b, c) = aaa(&mut a);
    c[0] = 9;
    drop(a);
    let d = c;
}
let mut a = [6u8; 2];      // type has ‘a
let (b, c) = aaa(&mut a);  // b and c has type &’a _;
drop(a) 
// due to type a, is an array it is move semantics
// Invalidates all type &’a _;
// You are allowed to move types as long it doesn’t conflict
// with using the invalidated references afterwards

// But we are so
let d = c;
// due to Non-lexical lifetimes 
// so we are “extending” the lifetime

Conflicts error appear but if we change the function signature

fn aaa<'a, 'b>(arg: &'a mut [u8;2]) -> (&'a mut [u8], &'a mut [u8]) 
// to
fn aaa<'a, 'b>(arg: &'a mut [u8;2]) -> (&'a mut [u8], &'b mut [u8]) 
// notice slight change on return parameters

so

fn main() {
    let mut a = [6u8; 2]; // type has ‘a
    let (b, c) = aaa(&mut a);
    // type b has &’a _ while c has &’b _
    // Unbounded lifetime!
    c[0] = 9;
    drop(a);
    // Invalidates all &’a _
    let d = c;
    // c is &’b _ so not invalidated
    // Undefined behaviour through use after free
}

Here another example

struct A<'a> {
    a: &'a str,
}
impl A<'_> {
    fn a<'a, 'b, 'c>(arg: &'a mut A<'b>) -> &'b mut A<'c> {
        unsafe {&mut *(arg as *mut _ as usize as *mut _)}
    }
    fn b<'a, 'b>(arg: &'a mut A<'b>) -> &'a mut A<'b> {
        arg
    }
}

With associated function “a” being unsound and possible ub
While associated function “b” being safe
Applying it here

fn main() {
    let a = String::new(); // has ‘a
    let mut aaaaa = A {a: &a}; // contains &’a _
    // But now SHOULD have has ‘b but doesn’t 

    let c = A::a(&mut aaaaa);
    // c is SHOULD be type &’b mut _ BUT is not
    // c has &’a mut A<‘c> ‘c is unbounded

    drop(aaaaa);
    // Invalidates &’b _ but there is none
    c.a = "aaaaa"
    // Another use after free
}

But what interesting is that changing

drop(aaaaa);
// to
drop(a);

It errors as

drop(a)
// Invalidates &’a _
let c = A::a(&mut aaaaa)
// c has type &’a mut A<‘c> 
// while the argument has &’b mut A<‘a>
// due to the function signature

Note that:

// Also lifetime annotations differ from the “lifetimes labels” applied by compiler
// e.g
fn a<‘a>(a: &’a u8) -> &’a u8 {
     a
}
fn main() {
   let local = 5u8;
   let one_ref = &local; has type ‘1
   let two_ref = &local; has type ‘2
   let three_ref = a(two_ref) has type ‘3
}
// &T is copy so copy semantics
// Lifetime annotation differs from actual lifetimes viewed by the compiler

To have even more bad signature such by
changing signature

fn a<'a, 'b, 'c>(arg: &'a mut A<'b>) -> &'b mut A<'c>
// to
fn a<'a, 'b, 'c, 'd>(arg: &'a mut A<'b>) -> &'d mut A<'c>

Both of them are unbounded so dropping either of them will compile
(no good as use after free)
Of course

struct Newtype(u8);
fn main() {
    let mut a = Newtype(5u8); 
    let refe = &mut a;
    let I_know_a_guy_whos_know_a_guy = &refe;
    drop(a);
    // Invalidates &’a _
}

Now this model would dangle the “I_know_a_guy_whos_know_a_guy
But I am assuming there is the implicit bound of ‘b: ‘a
So its invalidating ‘b as well to prevent dangling references
Bounds, now I understand little bit as such there are other cases of compiler extending lifetimes. I heard it here,

Here this example

struct A<'a> {
    a: &'a str,
}
struct Phantom<'a> {
    a: core::marker::PhantomData<&'a mut ()>,
}
impl A<'_> {
    fn b<'a, 'b, 'c>(arg: &'a mut A<'b>) -> (&'a mut A<'b>, Phantom<'c>)
    where
        'a: 'c,
    {
        (
            arg,
            Phantom {
                a: core::marker::PhantomData,
            },
        )
    }
}
fn main() {
    let a = String::new();
    let mut aaaaa = A { a: &a };
    {
        let (mut c, mut phantom) = A::b(&mut aaaaa);
        aaaaa.a = "aaaaa";
        let c = phantom;
    }
}

This errors due to the bound of ‘a: ‘c, it can be fixed by commenting on the bound, or commenting on “let c = phantom” (due to non-lexical lifetime) but it can also error if the bound is there (but not “let c = phantom”)
Using

impl Drop for Phantom<'a> {
    fn drop(&mut self) {/*...*/}
}

The type will then be dropped at end of scope or a function consuming the type rather than “borrowing” it.
Now I get little of for<‘a> but not alot,
Of course I heard closures are “opaque” syntactic sugar so for<‘a> is used like fn a<‘a>(blah blah blah) -> blah but applied to for<‘a> | blah | { blah }

Can anybody give feedback please (if your wondering what point of this post) and how improve this model of understanding lifetimes or is it good enough

When T: 'a is needed though? and what effects it does and again how can I implement this into my model or do you suggest an different model of understanding lifetime

This is a long post and I don't have time to reply to everything, but this is wrong. Lifetimes are durations in which something is borrowed. The value a does not have a lifetime. The lifetime begins at the &mut a line because that is when the value a becomes borrowed. If you later borrow a again, then you get a different lifetime because they are different borrows even if both are from a.

The meaning of T: 'a is that the type T does not contain any lifetime annotations shorter than 'a.

4 Likes

Well, I don't know if I really got a good feel for your overall mental model through the examples given. But at least part of your model seems to be associated Rust lifetimes with the the liveness of the referents, or with the (non-lifetime-parameterized) type of the referents. IMO models which do this fall short and cause confusion.

(I'm talking about things like let mut a = [6u8; 2]; // type has ‘a here.)

Despite the unfortunate overlap in terminology, Rust lifetimes -- those 'a things -- are associated with the duration of borrows, not what is commonly called the lifetime of values. The borrow checker makes sure there are no uses of variables (and other places) that conflict with any active borrows of that place.

Going out scope / being destructed is a use which conflicts with being borrowed, so they are connected. But the compiler does not assign Rust lifetimes to variables and then transfer those lifetimes to references or the like. Lexical scopes are also not assigned Rust lifetimes, despite the illustrations in many learning materials.

I'll next review your examples, speaking from my mental model, which is based around how the current borrow checker (NLL) is implemented:

  • Taking a reference borrows some place and creates a lifetime associated with the borrow
  • Where the lifetimes are active is calculated based how values are used, the signatures of functions which are called, and so on
  • When and how places are borrowed is calculated, based mostly on the lifetimes
  • Uses of places incompatible with active borrows are errors
  • Violations of lifetime bounds are also errors

Talking about the first example, beginning before the edit...

That type has no (Rust) lifetime. There are no lifetimes in the example until a borrow is introduced:

//               vvvvvv introduction of an exclusive borrow of `a`
let (b, c) = aaa(&mut a);

Uses of values whose type have a lifetime are one of the main determinators of where lifetimes are borrowed.[1] In the first example, uses of c determine where 'a is active, which corresponds to where the variable a is (exclusively) borrowed.

Accessing a while it's exclusively borrowed is an error, so you can't make a copy to pass to drop.

(std::mem::drop isn't magical[2] and [u8; 2]: Copy, so there would be no actual destructor or uninitialization due to a move of a there. The error is because you're trying to access something that was exclusively (mut) borrowed. "Invalidates all &'a _" is also somewhat incorrect -- copying a is compatible with a being shared-borrowed. So this compiles.)

The function is unsound after the edit, and the program has UB. Ignoring that, why wasn't there a borrow checker error? It is because there are no uses of b to keep 'a (the borrow of a) active. There's no relationship between 'a and 'b, so uses of c (keeping 'b active) does not keep 'a active either.

Thus there is nothing to keep the borrow of a active anymore, and the borrow checker error in main goes away.


On to the second example...

    let a = String::new(); // has ‘a
    let mut aaaaa = A {a: &a}; // contains &’a _
    // But now SHOULD have has ‘b but doesn’t 

    let c = A::a(&mut aaaaa);
    // c is SHOULD be type &’b mut _ BUT is not
    // c has &’a mut A<‘c> ‘c is unbounded

    drop(aaaaa);
    // Invalidates &’b _ but there is none
    c.a = "aaaaa"
    // Another use after free

Like the last example, there is no Rust lifetime associated directly with variable a. Now let us say variable aaaaa has type A<'y>. Then uses of aaaaa, or values of other types that contain 'y,[3] will keep the (shared) borrow of a alive.

When you call A::a(&mut aaaaa), let us also say you passed in &'x mut A<'y> and got back &'y mut A<'z> as per the function signature. We have the implied bounds 'z: 'y and 'y: 'x. 'x does not appear in the return type, and nothing that keeps it active appears in the return type (there's no 'x: 'whatever), so there is nothing to keep the exclusive borrow of aaaaa -- associated with 'x -- active after the call to A::a.

That is why there is no borow checker error here:

drop(aaaaa);

Nothing is keeping aaaaa borrowed.

// Another use after free

("Use after free" typically refers to using heap memory obtained from a allocator after freeing that memory,[4] which results in a segfault. The example does not correspond to that situation. However, the example is using memory that is uninitialized because the value has been moved out of,[5] so it is still UB.)

The return type of A::a contains 'y, so uses of c do keep the borrow of a active. It's an error to move a while it is borrowed, which is the error that you get.

Now you've broken the chain of lifetimes bounds back to any part of the input types from the return type, so uses of the return value don't keep any existing borrows (&a or &mut aaaaa) alive.


struct Newtype(u8);
fn main() {
    let mut a = Newtype(5u8); 
    let refe = &mut a;
    let I_know_a_guy_whos_know_a_guy = &refe;
    drop(a);
    // Invalidates &’a _
}

You can view the use of a (moving a) as invalidating both of those borrows, since moving is incompatible with being borrowed. There's nothing keeping the borrow of a active at or after the drop(a), so there is no conflict.


Yes -- uses of 'c will keep 'a active, which will keep aaaaa exclusively borrowed.

Do you mean this scenario, with the 'a: 'c bound but without c = phantom, which still exhibits an error?

Assuming so: phantom goes out of scope at the end of the block regardless, but without the impl Drop it has a trivial destructor, and the compiler recognizes that it does not observe the borrow associated with 'c. When you add the Drop for Phantom<'a>, the Drop::drop method is considered "within its rights" to observe the borrow,[6] and the compiler therefore assume it does.

In other words, the non-trivial destructor is a use of Phantom<'a> that forces 'a to be active at any point of destruction.

There's an unstable, unsafe way to promise the compiler you won't observe anything associated with the lifetime to avoid the error. All the std collections use it (which means on stable they are more magical than any local or third-party container.... which is convenient,[7] but also unfortunate[8]).


  1. Others include bounds between lifetimes, inferred or explicit; and annotations, like an explicit variable type, function signatures, or turbofish. ↩︎

  2. it is just a function that takes an arg by value and does nothing with it, letting it go out of scope and be destructed; it is also not the same as Drop::drop, which cannot be directly called ↩︎

  3. or some other lifetime that forces 'y to remain active due to lifetime bounds ↩︎

  4. giving the memory back to the allocator ↩︎

  5. unlike the first example, A<'_> is not Copy ↩︎

  6. it's considered a non-breaking change to alter the code so that it does so ↩︎

  7. as more sound programs are accepted ↩︎

  8. as it is more complicated and more surprising when non-std types act differently ↩︎

1 Like

That was my fault, that just my miscommunication however I am not sure if change the fact,
What I meant is that is not type has lifetime ‘a, I meant type has a tag of lifetime of ‘a (Also this just me modelling how lifetime and such may not be like inner working of the borrow checker) (I am not sure how borrow checker works internally, but I was assuming it keeps track on referee and references by checking if the referee is moving or influenced by drops using annotation since I remember that using you said “global analysis is brittle and not the best solution”, not saying its bad I am assuming you said it), the lifetime of that type I think is ‘static (as this type has no non-static references)

That I knew of, but I didn’t know [u8; 2] is copy, I thought it had move semantics, I assumed array types are not copy, so drop works (like you said it just moves and the destructors is called at the end of the function body, I also thought the lint was wrong in array types)
Of course looking at the criticism about my bad model, I was assuming the type was move semantics; not copy semantics but still I consider the criticism. Also the link; I thought (not in a offensive way) that, don’t empty slices and references referring to empty slices have a static lifetime and such coerce into return types

I thought it more general cases like freeing memory that still pointing to but yours more specific and still better than my definition

I am little confused with your words but I thought it due to non lexical lifetimes as such types without impl drop, the rust compiler can just drop it early if its unused later in the source code usually before end of scope or if the pointee type is moved (assuming not copy)

That I am confused of, what do you mean?

The first bit, I understand but the active borrow stuff, what do you mean again?

What do you mean again?
I am not sure but I could be extraneously extrapolated, that ‘c and ‘d has implicit bounds.
Sorry for asking same questions in some cases

What do you mean with “active borrow”?

In both cases I was just talking about your labels like

let mut a = [6u8; 2];      // type has ‘a

If you write a literal &[] it will be promoted, and will act like a &'static [T].[1]

A borrowed slice that results from reborrowing through an existing &[T] (or through a &Vec<T> etc) can't be promoted, even if it is empty.

Slices weren't really my focus for that playground though. Let me try to make my point another way.

This fails because copying a is in conflict with a being exclusively borrowed.

    let mut a = [6u8; 2]; 
    let r = &mut a;
    drop(a);
    println!("{r:?}");

This compiles because copying a does not conflict with being shared borrowed.[2]

    let a = [6u8; 2]; 
    let r = &a;
    drop(a);
    println!("{r:?}");

This fails because moving a conflicts with a being borrowed (shared or exclusive).

    #[derive(Debug)] struct NotCopy([u8; 2]);
    let a = NotCopy([6u8; 2]);
    let r = &a;
    drop(a);
    println!("{r:?}");

From the point of view of invalidating borrows: Moves invalidate all borrows, but copies only invalidate exclusive borrows.

The borrow checker does not change where values drop.[3]

When a value drops, such as going out of scope, that is a use of the value. How that use effects borrow checking depends on the type of the value. At a minimum, the place of the old value becomes uninitialized, which means the place itself cannot be borrowed. That is the only effect for types that have no destructor, such as integers, or any T: Copy.

If the type has some destructor,[4] that is a more involved use of the value, which is usually more like taking a &mut _ to the value. Among other things, this usually means that any lifetimes must still be active. That's what is going on in the phantom example; the destructor keeps the lifetime, and its associated borrow, active. The may_dangle attribute allows (unsafely) tuning which lifetimes remain active.

If a value is unconditionally moved along all paths before the end of its drop scope, the compiler recognizes that there will not actually be a drop at the end of the drop scope, and removes the drop. If passing a value to a function fixes a borrow checker error, that's probably the reason why. (The function may be std::mem::drop, but like I mentioned, that's not a magical function.)

If a value with a type that contains 'lt1 is used somewhere, 'lt1 must be active at that point.

let mut a = 1;
// Say this borrow has lifetime `'lt1`
let borrow = &a;

a = 2;

// This uses means `'lt1` must still be active, so `a` remained borrowed
// through the overwrite above (`a = 2`).  That causes an error.
println!("{borrow}");

If we have 'lt0: 'lt1, that means that 'lt0 is active at least wherever 'lt0 is active. So if a value with a type that contains 'lt1 is used somewhere, both 'lt1 and 'lt0 must be active at that point.

let mut a = 1;
// Say this (exclusive) borrow has lifetime `'lt0`
let exclusive = &mut a;
// Say this (shared) reborrow has lifetime `'lt1`
let borrow = &*exclusive;

let copy = a;

// This uses means `'lt0` must still be active, so `'lt1` must still be
// active through the copy above.  That means `a` is still exclusively
// borrowed when the copy is attempted.  That causes an error.
println!("{borrow}");

Before, the return type was &'y mut A<'z> with 'z: 'y and 'y: 'x. Uses of c kept the borrow of a active (the borrow of a is associated with 'y), but did not keep the borrow of aaaaa active (the borrow of aaaaa is associated with 'x).

After the change in signature, the return type is &'d mut A<'z> with 'z: 'd and 'y: 'x. There's no relationship between 'd and 'x or 'y; there is no relationship between 'z and 'x or 'y. So uses of the returned value c do not keep the borrows of a or aaaaa active.

let mut a = 1;
// `a` gets borrowed here.
let borrow = &a;
// Overwriting conflicts with being borrowed, yet this compiles.
// So the borrow must not be active here.
a = 2;
// (End of function -- no further uses of `borrow`)

  1. when T: 'static ↩︎

  2. This also demonstrates that std::mem::drop does not change the actual drop scope of a. You're just calling a normal function, passing in a copy of a. ↩︎

  3. More generally: borrow checking is a pass-or-fail test which does not change the semantics of the program. ↩︎

  4. a more general concept than types that implement Drop ↩︎

Absolutely not! Never! Drops couldn't be moved! Drop can, among other things, access something outside of your program (e.g. it may turn off light in the building when “everything is done”). You absolutely cound not move drops anywhere! Drops are not related to borrows, except as one of the functions that may access variable (even if invisible one). And drops have to happen where they are inserted: either when you move variable into function or expression that drops it (did you knew that if you write foo; said expression drops that variable), or, if nothing else consumes the variable, they are dropped at the end of scope.

Oh, So I thought it did, nevermind,
Because this compile

fn main() {
    let mut a = 5u8;
    let refe = &mut a;
    let refe2 = &mut a;
}

The type has 2 mutable references but because it not used (both of them) later, it doesn’t error, I thought it drop early as an explanation but more with non lexical lifetime I assume again

So it is the bounds what allow borrow checker know what types is associated and when referee is moved (or scopes {}), knows what references are invalidated? + other factors?

Yes, it's NLL.

fn main() {
    let mut a = 5u8;

    let refe = 
        // `a` is borrowed for `'1`
        &mut a;
    // `'1` ends (nothing keeps it active beyond here)

    let refe2 = 
        // `a` is borrowed for `'2`
        &mut a;
    // `'2` ends (nothing keeps it active beyond here)

}   // `refe2` goes out of scope (does not keep `'2` active)
    // `refe1` goes out of scope (does not keep `'1` active)
    // `a` goes out of scope

(References, even &mut _ references, do not have destructors.)

But you don't have any drops there. Types with and without drops are very different. C++ does include certain relaxations where certain constructors and destructors may be elided, but Rust never does that. Drops are simply a functions that are invisible and called in certain points to “release” an object.

It's similar in Copy trait, to (which you also misunderstood when you assumed that [u8; 2] wouldn't be Copy): that trait is kind of “anti-Drop”: while type with drop glue would call said drop glue in predictable moments (these are “invisible code cleanups”) types without drop glue may be of two forms: some of them don't have Drop, yet, nonetheless, couldn't be freely copied (simplest example is mutable reference: it doesn't itself have Drop, but if you wouldn't make “old copy” inaccessible then the whole safety of Rust would crumble), but some can leave a copy “behind” after move (e.g. shared references: they are, by definition, shared… there may be as many of them as one may want) — these are marked as Copy to simplify language use.

Making “old copy” of [u8; 2] invalid after you have created a copy would be pointless, it wouldn't help with safety — and that's exactly what lack of Copy is supposed to do!

So your saying like

fn main() {
    let a = 5u8;
    let b = a;
    let c = a;
}

This works with types that are copy (copy semantics) using the marker trait while without copy; this is invalid (that the point)
(Of course, this example can be generalised to more copy types but I want make it simple)
The Copy and Drop are exclusive (but you said

Is there an exception)

So what I interpreted of what your saying, is something similar like

fn main() {
    let mut a = 5u8;
    let b = &mut a;
    drop(a);
    let c = a;
}

This works because the u8 is copy and there is no point for the drop function since it invalidated a old copy of u8.
But

struct NewType(u8);
fn main() {
    let mut a = NewType(5u8);
    let b = &mut a;
    drop(a);
    let c = a;
}

This wouldn’t work because the lack of Copy trait “#[derive(Clone, Copy)]”
Correct me if I’m wrong

So do you not recommend this way of thinking of this kind of model

let a = NewType(5); // Tag of ‘a (Not “a” having lifetime of ‘a)
let b = &mut a; // Type &’a _

But rather knowing common implicit bounds and the relationship to referee and references
Like

struct A<'a> {
    a: &'a NewType,
}

And

let c = &mut A {a: &a} // that the &mut is the association of the borrow
// and the A<'a> is where is an association of another borrow

Rather than the “tag-based referee and references” model?

No, you are absolutely right. The one thing that people, often, stumble in the beginning is the fact that there are two entirely different drop function. One that's “magical” and that you need to implement to have a non-trivial drop is called Drop::drop: that one is magical, it's an opposite of Copy (you couldn't implement both Drop and Copy, these are both deviations from “this is type vulgaris” behavior, etc).

But that drop that you call in your examples, that one is a very different drop with the full source looking like this:

pub fn drop<T>(_x: T) {}

It's not magical in any way, shape and form[1]!

Any use of variable would have the same effect. Up to and including the simples possible use :

    a;

This drops the variable as efficiently as call to drop(a) or let _ = a; does. Both “move out” the value out of a and since there are no new consumer that would keep it alive… it's dropped (if there are a drop glue present).


  1. Not 100% true: it's maked as a language item for clippy to recognise it, but that's ignored by compiler. ↩︎

Can you explain in more detail what is drop glue. Of course you explain shortly but I want to know more about? Why is called drop glue?

It's explained in RFC 1857 that, surprisingly enough, happened after release of Rust 1.0 (I was surprised because I never thought it could ever be non-specified in a language like Rust).

Absolutely no idea, but probably because it's different from impl Drop.

The story here is that, of course, any type that does have impl Drop done for it have a drop glue: you told the compiler that disappearance of that type needs to be handled specially… compiler have to obey.

But types that don't explicitly implement Drop trait can also do “something special” when they are disappearing: compiler creates special invisible function that's called when that type disappears and that function just calls Drop:drop for all parts of that type.

The fact that type does or doesn't have a drop glue is important (you couldn't make type Copy if it has a drop glue and borrow rules are different for types with and without drop glue), but, surprisingly enough, you couldn't tell the compiler about arbitrary type whether it does or doesn't have a drop glue. Something like T: DropGlue or T: !DropGlue was proposed but never implemented…

Ok

I don't know what the “tag-based referee and references” model is as a whole. But I do not recommend associating Rust lifetimes ('a things) with values like [0; 2]. Doing so is associating a lifetime to a type that has no lifetimes, or is conflating Rust lifetimes (which most closely correspond to borrow durations) with the liveness scope of the referent. Those tend to cause a lot of confusion in my experience.

I recommend knowing how the borrow checker works at a high-level:

  • Determining where in the code places are borrowed, and how (exclusively or shared)
  • Checking all uses of all places to see if there is a conflict between a borrow and the use
  • Also checking that any lifetime constraints are satisfiable

And within that context,

  • Lifetimes are an approximation of where places are borrowed (a "borrow duration")
  • Uses of values whose type includes a lifetime determines what the lifetime is (the "where")
  • Lifetime annotations and relationships ('a: 'b) also contribute
  • Going out of scope, being moved, and being destructed are uses of values

I find that most borrow checker behavior can be explained with this as a baseline model.

For any given model, one can generally find some exception or nuanced case to pile on top, like "std collections have special drop behavior" or "overwriting references can release a borrow". And the compiler is evolving to soundly support more use cases. So I also recommend getting to a place where you're generally comfortable with the borrow checker (which may involve building experience by coding), and not expecting to understand every last detail upfront. Your mental model will become more complete over time. Also feel free to ask questions about particular code examples.


This thread has involved talking about how borrow checking works across function call quite a bit. When it comes to signatures specifically, IMO the key to first understanding a signature like

fn example<'a>(one: &'a String, two: &'a String) -> &'a str { ... }

is to understand that uses of the return value will keep *one and *two borrowed, due to the lifetimes being the same. That plus knowing what lifetime elision in functions mean will go a long way. You also need to recognize that the function signature determines how things stay borrowed, and not the function body. (Both the caller and the function body must obey the signature.)

Then you can move on to

fn example<'a: 'b, 'b>(one: &'a String, two: &'a String) -> &'b str { ... }
// and/or
fn example<'a: 'c, 'b: 'c, 'c>(one: &'a String, two: &'b String) -> &'c str { ... }

which are effectively the same in terms of how borrowing works (uses of the return value will keep *one and *two borrowed). There is just indirection though outlives bounds now, instead of requiring lifetime equality. You might think of this as a >= relationship, or a subset relationship (my preference), or a subtype relationship.

At the call site, the key thing to remember for these signatures is that uses of the return value determine the lifetimes of the inputs -- where the referents of the inputs remain borrowed. The compiler does not determine the lifetime of the output type by considering the lifetimes of the inputs. That is exactly backwards![1]


  1. even though it is how the book describes things ↩︎