Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using different lifetimes in struct implementations

Tags:

rust

I am trying to get a better understanding of Rust lifetimes.

struct Name<'a> {
    arg: &'a u8,
}
impl<'a> Name<'a> {
    fn new1(arg: &'a u8) -> Name<'a> {
        Name { arg }
    }
    fn new2<'b>(arg: &'b u8) -> Name<'b> {
        Name { arg }
    }
}

Is there any difference between functions new1 and new2? I am assuming it would matter if arg was &self? Is there any case where new2 implementation preferred or the other way?

like image 691
raj Avatar asked Oct 27 '22 09:10

raj


1 Answers

These two methods end up being exactly the same, but it's worth learning why. Along the way, we'll learn about lifetime coercion

One lifetime can be coerced into another lifetime if that second lifetime is shorter than (or rather, is contained by) the first. This is usually notated 'a: 'b, to mean that the lifetime 'a entirely encloses the lifetime 'b. The usual terminology is that 'a outlives 'b. The reasoning for this coercion is that you can always make a lifetime shorter if you need to. If a reference is valid during some lifetime, then it's also valid during any shorter lifetime contained in the longer lifetime.

So with that in mind, what sort of arguments can new1 and new2 take? We have a fixed lifetime 'a since the whole implementation is generic in that lifetime. However, new1 can not only take &'a u8, but any &'b u8 if 'b can be coerced to 'a. That is, 'b: 'a and 'b is longer than 'a.

new2 is slightly different, but it ends up having the same effect. The method is generic in the lifetime 'b and can take any &'c u8 if 'c: 'b. new2 is still technically generic in 'a, but since it doesn't use 'a at all, it can be ignored. That said, ignoring a generic parameter is confusing (why have the parameter at all?), so it would probably be best to use new1 instead.

Another reason to prefer new1 over new2 is that it fits better with Self. If we try to change the outputs to Self

impl<'a> Name<'a> {
    fn new1(arg: &'a u8) -> Self {
        Name { arg }
    }
    fn new2<'b>(arg: &'b u8) -> Self {
        Name { arg }
    }
}

the compiler complains. Why? Now the outputs have to be Name<'a> and in new2, we're returning Name<'b>. This isn't coercible to Name<'a> unless 'b: 'a, so we have to add that as a bound on 'b:

impl<'a> Name<'a> {
    fn new1(arg: &'a u8) -> Self {
        Name { arg }
    }
    fn new2<'b: 'a>(arg: &'b u8) -> Self {
        Name { arg }
    }
}

(playground link)

In this case, it's pretty clear that new1 is superior since it doesn't even need that second lifetime, but still allows exactly the same inputs.

like image 86
SCappella Avatar answered Jan 02 '23 19:01

SCappella