Scopes and lifetimes

Maybe it's not true (I am not sure), Thinking of values as having lifetime does not confuse me, current at least.

The value lifetime is the scope of a value keep alive(not moved, can be accessed). It is dynamic and can be compared, like 'a in &'a T, the difference is the shortage of an annotation, or a name.

There 2 types of life time.
A non-ref value, like String, only having type 1
A ref value. like &'a String, having type 1 as above, and another type 2 life time, e.g. 'a

drop(T) or just a plain T; does not affect type 2, but ends type 1 if T : Copy does not be satisfied.
There is only one exception: dropping a type 1 of the specific 'static, e.g. dropping a static variable.

That can be explained as this:
The compiler can make any 'lifetime "ends here" if it see a dropping. And yes, it can define 'lifetime ending here naturally to eliminate the need of dropping, make dropping a reference meaningless, but without drop, compiler will not provide such a definition if necessary
While 'static has been defined explicitly, leaving no space for compiler to provide another definition.

The following example shows both can be used in a uniform manner.

fn main() {
    pub fn fx<'a, T: 'a, U>(_: &mut &'a mut T) -> (fn(&mut &'a U), fn(&mut &'a mut U)) {
        fn x1<'b, T: 'b>(_: &mut &'b T) {}
        fn x2<'b, T: 'b>(_: &mut &'b mut T) {}
        (x1::<U>, x2::<U>)
    }

    let mut m = 91;
    let mut m1 = "9".to_owned();
    {
        let mut n = &mut m; // introduces type 2 lifetime '1, and n's type 1 lifetime, let's call it 'n
                            // then '1 : 'n
        let (f1, f2) = fx(&mut n); // fixed to '1

        {
            let mut z = &mut m1; //  introduces type 2 lifetime '2
                                 // and z's type 1 lifetime, let's call it 'z

            let mut xxx = &z;  // here force type 2 life time '1 == '2
                               // then we can conclude that 'z : '1
            f1(&mut xxx);

            // compiles
            drop(n);        // this is end of 'n
                            // because '1 : 'n and  'z : '1
                            // 'z can keep alive.
            println!("{}", z);  // so z accessed without any problem

            // and the other case fails
            // drop(z);           // 'z ends here, because '1 : 'n and  'z : '1,  'n ends here
            // println!("{}", n); // so the access is not allowed.
        }
    }
}

You see, that helps to explain things in high-level.

It is exactly the kind of confusion being warned about. T: 'static is a lifetime bound, which is totally different from the duration of any particular borrows of a given place of type String. It merely means that String contains no temporary references valid for shorter than 'static (because it contains no references at all), and thus it can be kept around for as long as the static lifetime if one so wishes. It has basically nothing to do with how long some Strings are borrowed, or how long a variable of type String actually lives. It is a capability, not a prescription.

6 Likes

Well, T: 'static is a different thing.
What I am saying is not T : 'static, but rather 'static : T, something confusing enough.

It seems to be right, but less meaningful and it makes things more complicated.
I will modify the original text, keep non-ref T only having one type lifetime.

When I say it seems to be right, I am saying:
a value in form &'a T, can be moved to the scope where 'a ends, for example:

let N = 9;
let n0 = {
    let n1 = {
        let n2 = &*N;
        n2
    }
    n1
}
n0;

n2 here having a form &'a i, and 'a can be assigned or defined to having a lifetime less or equal to where N ends, so it is allowed to be moved out to the block where N ends.

And a 'static : T means T can be move to any upper block.

let t = {{{{{... {let n: T = T::default(); n} ...}}}}}

No matter how many levels n is nested, it is OK to move out to where 'static ends, and it is everywhere in fact (forget Send, it is another topic)

This is T: 'static.

2 Likes

T : 'a means any reference held in T outlives 'a.
T : 'static means any reference held in T outlives 'static.
It does not mean T outlive static, only means it can be move to a block where 'static ends.
When it keeps position and does not been moved out, it is dropped at end of the the containing block or earlier by a move.

Because a non-ref T does not hold any ref, so it is true.

The following is another view:

Well, a value r having the form &'a T requires 'a : T, meaning 'a outlives r, and r can be moved out to same block where 'a ends. and non-ref T can be moved out to the same block where 'static ends, in a uniform manner, it is 'static : T.

Yes, it does not mean much, I have deleted the text.

Variables like x have liveness and drop scopes, but they don't have lifetimes

I find this statement a little confusing, as both the original book talks about the lifetimes of variables (the first example here is the same one we're talking about and it starts with "we've annotated the lifetime of r"), and so does Rust by example ("Specifically, a variable's lifetime begins when it is created and ends when it is destroyed.")? A variable (or, actually, the value the variable owns) is created and at some point is destroyed - that period of time is not the lifetime of that variable?

1 Like

It's all very simple.

There can be a thing in memory someplace. Some pattern of bits.

There can be another thing in memory some place, another pattern of bits, that indicates the location of the first thing. It can be used to access the first thing.

Clearly using the second thing before the first thing is brought into some meaningful state is a bad idea. Similarly using the second thing after the first thing has ceased to be anything meaningful is a bad idea.

Hence "lifetime". The first thing had better exist during the lifetime of the second thing.

Call these things "data", "variables", "bindings", "pointers", "references" whatever you like.

:slight_smile:

(I split the topic as I didn't want to derail the general feedback thread with this more particular area of discussion -- especially as there's a chance it will veer into the incredibly technical should I or someone else dive into the code above.)

Yes, to be clear, you are certainly not unique in referring to the liveness or drop scope of a variable its lifetime, and you could just ignore my suggestion on that front. But a "lifetime" in Rust also has a different meaning -- lifetime parameters which represent some region of validity for a borrow. In my experience, using the same term for both becomes problematic if/when one tries to build an intermediate or advanced understanding of lifetimes ('_) or the borrow checker. I'm basically following the lead of the NLL RFC here:

And based on the discussions I've had, some specific problems that come up if one uses the same term are

  • At just a surface level, "the lifetime of the &str" becomes ambiguous. Do you mean the lifetime in the type or the scope of the declared variable? Though this sounds trivial it can result in a lot of back and forth and confusion
  • People see "this scope has a lifetime" diagrams and end up with the "T: 'a is referring to the scope of the value" misconception
  • People too easily confuse the lifetime of a borrow with the liveness scope of the borowee as the same thing
  • People too easily confuse the lifetime of a borrow with the scope of the borrow variable itself
  • People too easily confuse liveness scopes with lexical scopes (which are more visible)

I've experienced this enough that I feel the distinction should be made from the start, and that conflating the two incurs a kind of "knowledge debt" -- something you'll have to unlearn either. And yes, I feel even The Book is wrong to conflate the two.

I've had a lot of borrow checker discussions on both the mental-model and technical level, so I may feel stronger about the distinction than most :slight_smile:


Personally I find this similar to how it's not uncommon for introductory material [1] to refer to things in Rust as mutable vs. immutable instead of shared vs. exclusive. It conveys a close enough idea to get started, but the ideas are somewhat skewed from how the language actually works, and you have to unlearn and relearn things once you run into interior mutability, and also when encountering challenges due to &mut requiring exclusiveness even if no actual mutation takes place.


  1. and other material ↩︎

4 Likes

Thanks for that. I started reading through The Rustonomicon's section on subtyping after you made your original comments, and started thinking about how a "reference" has two lifetimes, the one of the reference itself and the one of the value it refers to, and you're right - it gets confusing fast. :stuck_out_tongue:

I was thinking about rewriting the intro to that chapter to focus more on the compiler's perspective. When you typecheck a call to fn foo(x:i32) -> i32, you just need to make sure the types match up, but when you typecheck a call into fn foo(x: &i32) -> &i32) then x here isn't just a reference to a type, really the reference type has a type it refers to and some lifetime information, and the compiler needs to make sure the passed in value and return value satisfies both.

Does that sound like a reasonable way to explain this?

Hmm, let me go back and consider the actual example.

Ok, so here

// Let's avoid macros below so exactly what's going on is more clear
fn print(r: &i32) { println!("r: {}", r); }

fn main() {
    let r;                // ---------+-- 'a (scope? type lifetime?)
    {                     //          |
        let x = 5;        // -+-- 'b  | (scope - i32 has no lifetime)
        r = &x;           //  |       |
    }                     // -+       |
    print(r);             //          |
}                         // ---------+

It's not that type checking the call to a fn(&i32) failed per se, for example this gives the same error:

fn main() {
    let r;                // ---------+-- 'a (scope? type lifetime?)
    {                     //          |
        let x = 5;        // -+-- 'b  | (scope)
        r = &x;           //  |       |
    }                     // -+       |
    r;                    //          |
}                         // ---------+

It's that the lifetime of the type of r is capped by the scope of x ("no dangling references"), but we tried to use r outside of the scope of x. If we remove that use

fn main() {
    let r;                // ---------+-- 'a (lexical scope I guess)
    {                     //          |
        let x = 5;        // -+-- 'b  | (scope)
        r = &x;           //  |       |
    }                     // -+       |
}                         // ---------+

Everything compiles. Or if we change the scope of x -- let's take a look at the "fix" example:

fn main() {
    let x = 5;            // ----------+-- 'b (scope)
    let r = &x;           // --+-- 'a  | (lifetime of type???)
    println!("r: {}", r); //   |       |
                          // --+       |
}                         // ----------+

This looks sensible on the surface, especially if we've learned "local variables drop in the opposite order that they're declared". But this also compiles:

fn main() {
    let r;                // -------------------+-- 'a (lexical scope)
    let x = 5;            // ----------+-- 'b   |
    r = &x;               //           |        |
    println!("r: {}", r); //           |        |
                          // ----------+        |   
}                         // -------------------+

'a can't be the lifetime of the type of r as 'a still extends beyond the scope of x. NLL stands for non-lexical lifetimes, and before NLL, that did not compile. But we've had NLL that accepts this example on stable since July 2019.

So what is the lifetime of the type of r? That's a surprisingly complicated question in the most general context, but in this simple example, it's just the use sites of r.


Well, that was long winded, and I haven't answered the underlying question at all -- what's a good way to rewrite this section without presenting scopes as lifetimes? Honestly I'm not sure yet, and figuring that out is a lot harder than giving "...ehhh not really..." feedback :slight_smile:. Probably I need to just try and rewrite it myself.

2 Likes

I'd definitely give it a read, sounds interesting. Maybe a good place to start is some definitions - scope vs lifetime, where the distinction lies, or what the problem is with conflating the two. Can't say I "got it" from your exampes :sweat_smile:

This is my take at a rewrite. If you have a minute, give it a quick read through and tell me what you think. (Hopefully it's better, otherwise I haven't learned anything. :wink: ) But if you want to give writing it yourself a try or make changes to this one, by all means do so! Like @ua-kxie I'd be interested to read your take on it.

OK, I gave it a shot by just rewriting each section in order. But honestly, I'm still not happy with it. (Probably I wouldn't be happy with the book either.)

In short I think a dedicated, freestanding section on borrowing and reborrows is needed to not be sort of woefully incomplete or misleading. But guides like this are aimed at letting readers "just get started", and I have yet to see something that strikes the balance of

  • summarizing "everything"
  • being brief / not overwhelming
  • not being inaccurate

At least, I haven't figured out a way. Maybe it just has to be along the lines of "look here's a taste so you're not blindsided, but just get started for now and come back to this tutorial (link) when you hit a wall or just want to understand more." I.e. give up on summarizing "everything" (and own up to "there's too much [detail/nuance/just too much] to explain everything here").


I sort of suspect the diff is more elucidating than the before or the after :sweat_smile:.

Anyway here's my shot at rewriting 10.3.

Validating References with Lifetimes

Every value in Rust has a scope - a point in the code where the value is created, and a point in the code where the value is destroyed. Every reference in Rust has a lifetime, which is a region where the reference is valid. When inferred within a function body, this region is typically the spans of code where the reference is used. The lifetime of the reference obviously needs to be shorter than the scope of the value it points to, as otherwise the reference would point to freed memory -- and such a reference cannot be valid.

In addition to the value going out of scope, there are other situations which can make a reference invalid. Moving the value would also cause a reference to dangle, for example. A &mut is an exclusive reference, so taking a &mut to the value also invalidates any pre-existing borrows. This constraint avoids undefined behavior such as data races, and related problems such as iterator invalidation.

The Rust compiler has a borrow checker which infers the lifetimes of references based on their use and any other annotations or constraints in the code (which we explore below). It also checks these inferred lifetimes against the scopes, moves, and other uses of values. If there are any constraints that cannot be met, or any uses of lifetimes that conflict with the use of values, they are reported as errors.

[1]

Preventing Dangling References with Lifetimes

One of Rust's memory safety guarantees is that references never dangle -- never point at memory that has been freed. Here's an example:

fn main() {
    let r;
    {
        let x = 5;
        r = &x;           // --+ 'r
                          //   |
    }                     // --💥--------------- `x` is freed
                          //   |
    println!("r: {}", r); // --+ last use of 'r
}

This won't compile. The variable r is used after the inner block, but it's a reference to x which will be dropped when we reach the end of the inner block. After we reach the end of that inner block, r is now a reference to freed memory, so Rust's borrow checker won't let us use it.

More formally, we can say that the scope of x is shorter than the lifetime of r, which we've labeled as 'r in the comments of this example (a strange name, but this is actually a bit of foreshadowing). The borrow checker sees that x goes out of scope before the end of 'r, and the borrow checker won't allow this.

This version fixes the bug:

fn main() {
    let x = 5;
    let r = &x;           // --+-- 'r
                          //   |
    println!("r: {}", r); // --+ last use of 'r
}                         // ------------------ `x` is freed

Here x goes out of scope after 'r, so r can be a valid reference to x.

[2]

Generic Lifetimes in Functions

Now for an example that doesn't compile, for what might not at first be obvious reasons. We're going to pass two string slices to a longest() function, and it will return back whichever is longer:

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";
    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}

// This doesn't work!
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

If we try to compile this, we get an error. When the Rust compiler checks a call to a function, it doesn't look at the contents of the function, only at the signature; the signature is a contract that both the function caller and the function body must uphold. Additionally, when you elide the lifetimes of input arguments as in this example, they are interpreted to be two distinct generic input lifetimes.

So the root of the problem here is that in this function, it is ambiguous from the signature alone whether the function is meant to return x or y or potentially either one. Since these have different lifetimes, the compiler considers the signature to be unacceptably ambiguous. Consider this example of calling this function:

fn main() {
    let string1 = String::from("abcd");
    let result;
    {
        let string2 = String::from("uvwxyz");
        result = longest(&string1, &string2);
    }
    // This shouldn't compile if a reference to `string2` can be
    // returned.  (But how can we convey that possibility to Rust?)
    println!("The longest string is {}", result);
}

Here if longest() returned the reference to string1, it would still be valid by the time we get to the println!, but if it returned the reference to string2 it would not.

We need a way to let the compiler -- and other humans who read our code! -- whether our function returns a reference with the same lifetime as the first argument, the second argument, or potentially either argument.

[3]

Lifetime Annotation Syntax

We fix this problem by telling the compiler about the relationship between these references. We do this with lifetime annotations. Lifetime references are of the form 'a:

&i32        // a shared reference with an anonymous or inferred lifetime
&'a i32     // a shared reference with an explicit lifetime
&mut i32    // an exclusive reference with an anonymous or inferred lifetime
&'a mut i32 // an exclusive reference with an explicit lifetime

A lifetime annotation on a single input argument variable isn't very meaningful. Lifetime annotations are primarily useful to describe constraints on the relationship between lifetimes (and thus, for example, between the lifetime of references), or between lifetimes and generic types.

Lifetime Annotations in Function Signatures

We can declare a lifetime annotation for a function in much the same way we add generic types. The lifetime annotation must start with a '. Typically they are single characters, much like generic types. And just like generic types, these will be filled in with a real lifetime for each call to the function.

We can fix the longest() function in our previous example with:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

longest a generic function (the syntax is similar for a good reason). We're saying here that to call this function, there exist some lifetime which we're going to call 'a, and the input references x and y both have the lifetime 'a, and we're going to return a &str with that same lifetime. Under this signature, it would be valid to return either x or y.

It's important to note that it's still possible to pass variables to the function which don't have the same lifetime at the call site. This is possible because shared references can be copied, and can also coerce from a long lifetime to a short lifetime. When a function takes a shared reference as an argument, this happens automatically if required. For example, when calling longest, a lifetime no longer than the shorter of the two argument lifetimes will be inferred. Depending on how the return value is used, a lifetime that is shorter than both arguments could be inferred as well.

The net result is that the return value of longest() will live no longer than the shorter lifetime of the references passed at the call site, and thus could be derived from either input. When the rust compiler analyzes a call to longest() it can now mark it as an error if the return value is used in a way incompatible with this constraint.

Returning to this example:

fn main() {
    let string1 = String::from("abcd");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(&string1, &string2);
    }
    // This doesn't compile!
    println!("The longest string is {}", result);
}

Here the compiler now knows that the return value of longest() can only be as long as the shorter of &string1 and &string2, so it knows that the use of result in the println! macro is invalid -- &string2 cannot be valid at that location.

[4]

Thinking in Terms of Lifetimes

The way we annotate lifetimes depends on what the function is meant to do. If we changed longest() to only ever return the first parameter, we could annotate the lifetimes as:

fn longest<'a>(x: &'a str, y: &str) -> &'a str {
    x
}

This tells rustc that the lifetime of the return value is the same as the lifetime of the first parameter.

The lifetime of the return value will generally have the same annotation as at least one of the input parameters (or be 'static, which we'll discuss in a moment).

The concrete lifetimes chosen for function lifetime parameters are determined by the call site and as a result, are always longer than the function body. Therefore you cannot create references to local variables with the lifetime of a function parameter at all, since local variables drop at the end of the function. And naturally you cannot return references to local variables either, as they would immediately dangle.

[5]

Lifetime Annotations in Struct Definitions

So far all the structs we've created in this book have owned all their types. If we want to store a reference in a struct, we can, but we need to explicitly annotate it's lifetime. Just like a function, we do this with the generic syntax:

struct ImportantExcerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().expect("Could not find a '.'");
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}

Again, it's helpful to think about this like we would any other generic declaration. When we write ImportantExcerpt<'a> we are saying "there exists some lifetime which we'll call 'a" - we don't know what that lifetime is yet, and we won't know until someone creates an actual instance of this struct. When we write part: &'a str, we are saying "when someone reads this ref, it has the lifetime 'a" (and if someone later writes a new value to this ref, it must have a lifetime of at least 'a). At compile time, the compiler will fill in the generic lifetimes with real lifetimes from your program, and then verify that the constraints hold.

Here this struct has only a single reference, and so it might seem odd that we have to give an explicit lifetime for it. However, having to declare all lifetime parameters avoids some ambiguities and footguns in the same way having to declare all variables does. Additionally, knowing whether or not a struct may borrow something is important information for those reading your code. It would be easy to miss that a field is a reference if the lifetimes could be completely elided in the definition.

Info: The original "The Rust Programming Language" here said that "this annotation means an instance of ImportantExcerpt can't outlive the reference it holds in its part field," but I found that not a helpful way to think about this - of course a struct can't outlive any references stored inside it. I found this answer on Stack Overflow to be a lot more illuminating.

Here's an example where a struct requires two different lifetime annotations (borrowed from this Stack Overflow discussion which has some other good examples too):

struct Point<'a, 'b> {
    x: &'a i32,
    y: &'b i32,
}

fn main() {
    let x = 1;
    let v;
    {
        let y = 2;
        let f = Point { x: &x, y: &y };
        v = f.x;
    }
    println!("{}", *v);
}

The interesting thing here is that we're copying a reference out of a struct and then using it after the struct has been dropped. This is okay because in this case the lifetime of the reference is longer than the scope of the struct. This is only possible because the x and y fields have distinct lifetimes (because Point has two lifetime parameters). If we had defined Point with only one lifetime parameter:

struct Point<'a> {
    x: &'a i32,
    y: &'a i32,
}

Then you will get a borrow check error, as the lifetime of Point cannot be greater than the scope of the inner block (due to taking a reference to y).

tip: Similar to trait bounds, we can add a lifetime bound to a lifetime annotation in a function or a struct.

struct Point<'a, 'b: 'a> {
    x: &'a f32,
    y: &'b f32,
}

You can read 'b: 'a as "'b outlives 'a", and this implies that 'b must be at least as long as 'a. There are very few cases where you would need to do such a thing, though.

[6]

Lifetime Elision

Way back in chapter 4, we wrote this function:

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}

How come this compiles without lifetime annotations? Why don't we have to tell the compiler that the return value has the same lifetime as s? Actually, in the pre-1.0 days of Rust, lifetime annotations would have been mandatory here. But there are certain cases where Rust can now work out the lifetime on it's own. We call this lifetime elision, and say that the compiler elides these lifetime annotations for us.

What the compiler does is to assign a different lifetime to every reference in the parameter list ('a for the first one, 'b for the second, and so on...). If there is exactly one input lifetime parameter, that lifetime is automatically assigned to all output parameters. If there is more than one input lifetime parameter but one of them is for &self, then the lifetime of self is assigned to all output parameters. Otherwise, the compiler will error.

In the case above, there's only one lifetime that first_word could really be returning; if first_word created a new String and tried to return a reference to it, the new String would be dropped when we leave the function and the reference would be invalid. The only sensible reference for it to return comes from s, so Rust infers this for us. (It could be a static lifetime, but if it were we'd have to explicitly annotate it as such.)

You can find more details and examples in the Reference.

Lifetime Annotations in Method Definitions

We can add lifetime annotations to methods using the exact same generic syntax we use for generic structs:

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }

    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

Here 'a refers to the lifetime of the struct itself, but thanks to lifetime elision, in announce_and_return_part(), the return value is automatically given the same lifetime as self, so we don't actually have to use it.

If the return will always be a copy or sub-borrow of your borrowed field, however, it may be more flexible for consumers of the method to receive the longer lifetime:

    fn announce_and_return_part(&self, announcement: &str) -> &'a str {
        println!("Attention please: {}", announcement);
        self.part
    }

[7]

The Static Lifetime

[I didn't have any changes after here]


  1. Notes on this section
    Mainly here I make a distinction between scopes and lifetimes. I also mention a couple of common ways to invalidate a reference besides things going out of scope. A constraint violation would be a useful code addition, which could be as simple as "you said you'd return a &'static but didn't".

    Just as rustc infers the type of many of our parameters, in most cases Rust can infer the lifetime of a reference (usually from when it is created until it's last use in a function). Just as we can explicitly annotate a variable's type, we can also explicitly annotate the lifetime of a reference in cases where the compiler can't infer what we want.

    For intra-body lifetimes, there's no way to directly annotate the inferred lifetimes as they have no names. You have to use tricks like the helper functions at the top of this thread. It's a niche technique and usually can just make things more restrictive. ↩︎

  2. Notes on this section

    The variable r is in scope for the entire main() function,

    If you don't take a reference to the r or entwine its lifetime with something else, it's scope doesn't really matter, because the destruction of a reference is a no-op. ↩︎

  3. Notes on this section

    When the Rust compiler checks a call to a function, it doesn't look at the contents of the function, only at the signature.

    This is true, [mumble mumble except return position impl trait] but the example doesn't compile even if we never call it. The API (signature) of a function is a contract that the compiler enforces both on callers, but also on the function body. The API itself is just considered inherently ambiguous here. I don't think it gets as far as borrow-checking, or rather if it does, it's a "we'll just assume 'static I guess so we can spew more errors that might or might not make sense" situation. ↩︎

  4. Notes on this section
    Rewritten to highlight that, yes, the lifetimes really are the same, and that this works due to lifetime shortening coercion and copy. Still missing: Explaining reborrows for the &mut case. ↩︎

  5. Notes on this section

    The lifetime of the return value must have the same annotation as at least one of the parameters

    It can be independent in function signatures (but not function pointer types or dyn Fn types). Such cases are pretty niche and many of them could be replaced by 'static. ↩︎

  6. Notes on this section
    I rewrote the part about not having lifetime declarations to a POV you may or may not agree with. Question though: why didn't you include the same argument about the compiler just figuring things out for type parameters?

    I rewrote the last example somewhat too. ↩︎

  7. Notes on this section
    Extended because in my experience, if you can return the longer lifetime, it's better for consumers of your type. This deserves an example but I haven't created one yet. ↩︎

6 Likes

I'll give this a read through tonight! (I have family over right now, so I’m eager to read this, but can’t right now.) But, I do notice this is based on my original version, and I wonder if you had a chance to take a look at this version? I'm very curious to know what you think. For me, the realization that a lifetime is part of the type of a reference in the same way that i32 in &i32 is made this really click for me (but maybe that's just how I'm wired and this doesn't make sense to anyone else. :P)

(And if you don’t have time to read it that’s fine too - you’ve been very generous with your time already!)

It might be half and half? At least, I didn't write everything in one go, and some sections were updated from my first read, heh.

Anyway, I'll take a look if I can.

I think you might mean

  • &i32 on it's own is a not a type
  • &'a i32 for some concrete lifetime is a type
  • (and there is no higher-ranked for<'any> &'any i32 type)

In which case, that was an "aha!" moment for me too. It comes up when people try to do this:

fn foo<R, Closure>(_: Closure) where Closure: FnMut(&str) -> R {}

fn main() {
    foo(str::trim);
}

And the error is because

  • The Closure needs something analogous to for<'any> fn(&'any str) -> R for some R
  • The R must be a single, concrete type
  • The signature of trim is like for<'any> fn(&'any str) -> &'any
    • So it can satisfy fn(&'x str) -> &'x str for any concrete 'x, but
    • It cannot satisfy for<'any> fn(&'any str) -> R for any concrete R

Or taking a step back, the bound

Closure: FnMut(&str) -> R {}

implies that R cannot depend on the input &str's lifetime.

1 Like

Actually, let me read through your notes here first - I’ll incorporate what I learn. :wink:

1 Like

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