About lifetime annotation

Hi, all

I have a question about explicit lifetime in function annotation.
Take look at the following picture:


I define a function which takes two String references as input. 'b : 'a requires that the lifetime of y is no smaller than x. When calling the function at line 6, I think there will be an error because s2 is in the lifetime of s1, however, the compiler doesn't issue an error. Why ?

Why do you require y to outlive (or live as long as ) x? You don't return anything that relates the two. I think you should just use 'a for both and be done with it. What you are trying to convey, is how the lifetime of the return value relates to the lifetimes of the inputs. You can only return 1 lifetime. Whether you return x or y, the lifetime of the thing returned is the lifetime of x or y. Neither needs to live longer or shorter than the other, so just just lifetime a' for both. That way, if you return x, your return value has lifetime 'a and if you return y, your return value has lifetime 'a, where 'a is the lifetime associated with x or y.

Lifetimes are not associated with values, they are associated with borrows. 'a and 'b in your example name the regions in which the borrows in the expressions &s1 and &s2 in your call to longest will be considered to still be borrowed.

Of course, that only just shifts the question from the values to the borrows. The following is still allowed, even though 'a clearly is larger than 'b:

let r1 = &s1; // 'a begins
let r2 = &s2; // 'b begins
longest(r1, r2); // still valid. Huh...

The deal here is that 'b : 'a doesn't mean so much that 'a is a subset of 'b, but rather that 'b outlives 'a. That is, it constrains what happens after the function. In practice, rust will choose 'a to be the smallest possible scope based on how the return value is used (since it appears in the return value), and the "outlives" constraint means s2 will also be considered to be borrowed for that period.

let s1 = String::from("world");
let borrow = {
    let r1 = &s1; // 'a begins

    let s2 = String::from("hello");
    let r2 = &s2; // 'b begins
    longest(r1, r2)
}; // s2 is dropped
// 'a is assumed to still be active because we stored a
//     borrow with that lifetime in `borrow`.
// 'b: 'a means 'b must also be active;
//     but that is impossible since s2 was dropped.
// This is an error.
1 Like

I know one lifetime annotation is enough for this function. But my question is not how to write the longest function. I just wonder why the compiler doesn't issue an error of the code above.

I'm not 100% sure, but I think this is precisely because 'b outlives 'a. The function says that the returned value will live at least for lifetime 'a IIRC, so a binding that can handle a larger lifetime wouldn't be an issue.
The converse (i.e. 'a : 'b) would not be true, as borrowck would rightly complain that the returned borrow doesn't live long enough.

EDIT: correct typo

'b : 'a meads/reads, "the lifetime b extends the lifetime a". In other, words, "b outlives a". Perhaps you mean "a' : b'"

1 Like

First you have the owned values s1, s1 any borrow lifetimes are strictly smaller.

The complier (I think) works backwards. It takes result and says where will be its end. At the end will borrowed item 'a be valid. The bound 'b:'a adds the constraint that the second borrow must be at least as long. I don't think the start points are relevant other than for determining the end of scope.

What you can't do during the life of result is mutate either s1 or s2 (assuming you added mut and then tried somehow after.)

Adding to @ExpHP

let result = {
    let r1 = &s1;
    let r2 = &s2;
    let r = longest(r1, r2);
    println!("{} {} {}", r1, r2, r);
    r
};

This is valid code. Instead of just been limited to the life of r1 and r2 a reborrow occurs making r then subsequently result be borrowed from s1 and s2 independently of the lifetime of r1 and r2.

Nit: This is not what is typically referred to as a "reborrow" in the rust lexicon. A reborrow is when a shorter borrow is produced from a longer one (by doing &*self or &mut *self; this also implicitly done on method receivers and function arguments).

I think the keywords you're looking for are "subtyping" and "variance", and has its own chapter in the nomicon.

Essentially, because you have the 'b: 'a relationship, the compiler can use subtyping to figure out that the lifetime of 'a is less than or equal to (or maybe greater than, I forget) the 'b lifetime. This means you can return a &'a String because both 'a and 'b satisfy the 'a lifetime bound, one because 'a == 'a, and the other because 'b is a superset of the 'a lifetime.

I believe the actual implementation is also explained in the rustc guide (I think the Type Inference and Variance chapters may be useful), but @mark-i-m and @nikomatsakis use big words which I haven't fully wrapped my head around yet :stuck_out_tongue_winking_eye:


A concrete example would be in a language like Java if I were to return an object or an instance of a child class, except we're using lifetimes.

public static Animal foo(boolean condition) {
  if (condition) {
    return new Dog();
  } else {
    return new Animal();
  }
}

(probably not even valid Java, but hopefully you get the gist)

The compiler tries to use the shortest concrete lifetimes which will satisfy the requirements laid out in the function signature—effectively, the intersection of concrete lifetimes. With shared borrows, it's possible to use a subset of a longer lifetime if that subset would be sufficient ('static is an extreme case of a reference which can be used anywhere because of this.)

In this case, the concrete lifetime used for 'a doesn't have to be the full lifetime of s1; it can be shortened to that of s2. That configuration now satisfies 'b: 'a (... lives at least as long as...). For the same reason, using 'a for all references in the signature would also work.

Lifetime shortening isn't done for mutable references, because doing so would let one sneak in a dangling reference. There are more intricacies wrt the precise details of the "outlives" relationship in various situation, most of which are documented in the Nomicon.

This is subtle, but &'a mut T is variant over 'a but invariant over T. As such, you can squeeze down mutable refs as well purely on the lifetime of the borrow if the T allows for it. In the String example used in this thread, you can make longest() take &mut String and it would work just as well.

I still don't get it. I declare that 'b is great than 'a. " it can be shortened to that of s2." ?

As others mentioned, the lifetime parameters are of the borrow, not of the lifetime (or scope) of the value (ie referent). When you declare a lifetime parameter, whether in an fn or a type (struct, enum, trait, etc), it’s a generic parameter - think of it like a T: PartialEq generic type parameter with a constraint. Unlike generic type parameters, however, the caller never specifies the concrete lifetime - it’s selected by the compiler. So whereas you can call a method expecting T: PartialEq with a type of your own choosing, so long as it satisfies the constraint, you never get to do this for lifetimes.

Instead, the lifetime constraints (eg 'b: 'a) are solved for by the compiler. So the compiler is presented a constraint problem, and it tries to solve it. The lifetime here is, to reiterate, a borrow lifetime - it is not the lifetime/scope of the value. The compiler picks the smallest borrow scope that satisfies the constraints. In this case, it can borrow a longer scoped value for a shorter borrow and still meet the constraints you’ve set out. This is similar to how you can use a single lifetime parameter 'a there for both arguments. It doesn’t mean that the values live for the same amount of time - it’s a relationship that you’re expressing (taking something with 'a lifetime, and returning something with at least 'a lifetime).

In fact, you cannot use lifetime relationships (ie outlives constraints) to require that one value lives longer than another - this is not supported in any way by the Rust type system.

The typical case of needing outlives constraints is when you have mutable borrows over invariant data - in that case, compiler does not allow shrinking lifetimes like in this case. That’s to prevent a longer lived type from being substituted for a shorter requirement, but then having a shorter lived reference given to it that leads to a dangling reference - the linked nomicon page goes into more detail on this so I won’t repeat it here. So when lifetimes can’t be shrunk down but you still want to modify some invariant type, you’ll need to provide the additional outlives constraints so the compiler will know that you’re not in danger of creating a dangling reference, and it will then check that when it substitutes concrete lifetimes for the generic ones.

Let me know if something’s unclear and I’ll try to elaborate. This is not an easy thing to explain (or understand) once you get into the nitty gritty. The whole thing is predicated on “can memory safety be violated otherwise” - how you describe/relate lifetimes is then based on that.

1 Like

I believe the correct thing to say here is, "and returning something with at most 'a lifetime" or "and returning something with a lifetime no longer than 'a"

Yes, that's true for sure. One thing that helped me to clarify is that when you are specifying lifetimes, you are doing so so that you can specify how the function's output value's lifetime relates to the lifetime(s) of the inputs. What you are trying to do is ensure that anything in the return value that references something in one of the input references/borrows does not attempt to outlive the thing that it references. That is all.

1 Like

Right, I was imprecise; I tried to convey the idea that working with mutable references can be much more restrictive.

The thing I wanted to actually convey here is you can return something, from an implementation standpoint, with a potentially longer lifetime (when variance/subtyping allows for it) such as 'static. It’s true that from signature (ie caller’s) standpoint, it doesn’t outlive 'a.

That’s certainly a good way to think about it. It’s not always about tying input to output lifetimes, however - you can have an fn that returns nothing but takes mutable and/or immutable references as arguments, and then you may need to express the lifetime relationship purely between the inputs.

1 Like

:+1:

Mutable refs is also when one may need to specify multiple lifetimes and/or outlives relationships between them. Immutable refs can usually get away with a single lifetime param for all of them.

1 Like

Wait, so is it implied that Rust's lifetime lengths correspond to alphabetical order?? I.e., the lifetime indicated by 'a is <= that indicated by 'b, which is in turn <= 'c, etc.?

If so, it would be nice to put this information in the Rust book, because it doesn't even hint at it (at least, this was the case a few months ago when I went through it). Maybe other people were somehow able to assume this rule correctly, but to me all that is indicated by the different letters is that the lifetimes are not required to be the same.