Define a function that returns either of its arguments, with different lifetimes


#1

Hello fellow Rust enthusiasts,

As the title suggests, I’d like to define a function that could return either of its arguments, provided those arguments are references with possibly different lifetimes. A short snippet worthing a thousand words, this would for instance looks like the following:

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

I guess having something as 'a or 'b would be a bit problematic with respect to lifetime analysis, but maybe there’s a way to force some kind of relationship between lifetime specifiers. That way, one could for instance assume that if 'b necessarily is longer than or as long as 'a, therefore it’s safe to assume the lifetime of the returned value is at most as long as 'a.

Is there any way to get close to what I’m suggesting?


#2

I’m not sure if I understand, but … where 'b: 'a means "such that 'b lives at least as long as 'a".


#3

Well, if you have two lifetimes, then one of them has to be longer than or equal to the other, meaning that you can do something like this:

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

#4

Thanks, that is exactly what I was looking for!


#5

Note that because of variance, @OptimisticPeach’s longest is equivalent to

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

The caller chooses an 'a that fits all three lifetime requirements:

  • doesn’t outlive x
  • doesn’t outlive y
  • isn’t outlived by the return value

These are the same requirements imposed by the two-lifetime version.

It can be kind of counterintuitive, but the thing to understand is that lifetime annotations don’t actually change lifetimes of a thing, they only express relationships between input and output lifetimes. Chapter 10.3 of the book deals with this same example.


#6

What @trentj is trying to explain, is that because of the implicit relationship between 'a, 'b: 'a and just 'a, they are practically the same; if you only require 'a, then you end up with a lifetime that is the actual lifetime of one parameter, and a coerced/equivalent lifetime of the other. For example:

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

fn main() {
    let str_a = "abc"; // 'a; first parameter but its lifetime isn't named
    {
        let str_b = "cba"; // 'b; second parameter, its lifetime isn't named either
        {
            //This works because 'b is equivalent to 'a in this context,
            println!("{}", longest(str_a, str_b)); 
        }
        //While 'b is shorter in this context
    }
}

#7

I mostly agree, but I want to clarify this part:

I think it’s misleading to say that a lifetime parameter ever takes the actual lifetime of an argument, because the compiler is always free to choose a shorter lifetime if it wants (as long as it outlives the connected output lifetime). So when x and y are references and you write longest(x, y), the compiler doesn’t have to choose one of those lifetimes; it just has to choose some lifetime 'a such that both x and y outlive 'a.

Here’s an example where the lifetime 'a can’t be the same as the lifetime of either input:

let mut s1 = String::from("foo");
let x: &str = &s1;
println!("{}", x);
let s2 = String::from("quux");
let y: &str = &s2;
let _l = longer(x, y);
s1.clear();
println!("{}", y);

The lifetime of x must start before its use in the first println! and must end before s1.clear() to satisfy the rules of references. Meanwhile, the lifetime of y must start after s2 is created and must end after its use in the second println!. The lifetime of _l can’t be either; it has to be smaller than both for this code to compile (which it does, in the 2018 edition).