Assigning different lifetimes to a single variable

Viewed 162

I'm still trying to understand Rust ownership and lifetimes, and I'm confused by this piece of code:

struct Foo {
    x: String,
}

fn get_x<'a, 'b>(a: &'a Foo, b: &'b Foo) -> &'b str {
    let mut bar = &a.x;
    bar = &b.x;
    bar
}

Playground

This code does not compile, because data from 'a' is returned. I assume this is because when I initialized bar, I assigned a &'a reference to it, so Rust assumes that bar has lifetime 'a. So when I try to return a value of type &'a str it complains that it does not match the return type 'b str.

What I don't understand is: Why am I allowed to assign a &'b str to bar in the first place? If Rust assumes that bar has lifetime 'a, then shouldn't it prevent me from assigning b.x to it?

2 Answers

Every borrow has a distinct lifetime. The Rust compiler is always trying to minimize lifetimes, because shorter lifetimes have less chance to intersect with other lifetimes, and this matters especially with mutable borrows (remember, there can only be one active mutable borrow of a particular memory location at any time). The lifetime of a borrow derived from another borrow can be shorter or equal to the other borrow's, but it can never be larger.

Let's examine a variant of your function that doesn't have any errors:

fn get_x<'a, 'b>(a: &'a Foo, b: &'b Foo) -> &'b str {
    let mut bar = &a.x;
    bar = &b.x;
    todo!()
}

The expressions &a.x and &b.x create new borrowed references. These references have their own lifetime; let's call them 'ax and 'bx. 'ax borrows from a, which has type &'a Foo, so 'a must outlive 'ax ('a: 'ax) – likewise with 'bx and 'b. So far, 'ax and 'bx are unrelated.

In order to determine the type of bar, the compiler must unify 'ax and 'bx. Given that 'ax and 'bx are unrelated, we must define a new lifetime, 'abx, as the union of 'ax and 'bx, and use this lifetime for the two borrows (replacing/refining 'ax and 'bx) and the type of bar. This new lifetime needs to carry the constraints from both 'ax and 'bx: we now have 'a: 'abx and 'b: 'abx. The borrows with lifetime 'abx don't escape from the function, and lifetimes 'a and 'b outlive the call frame by virtue of being lifetime parameters on the function, so the constraints are met.

Now let's get back to your original function:

fn get_x<'a, 'b>(a: &'a Foo, b: &'b Foo) -> &'b str {
    let mut bar = &a.x;
    bar = &b.x;
    bar
}

Here, we have an additional constraint: the type of bar must be compatible with &'b str. To do this, we must unify 'abx and 'b. Given that we have 'b: 'abx, the result of this unification is simply 'b. However, we also have constraint 'a: 'abx, so we should transfer this constraint onto 'b, giving 'a: 'b.

The problem here is that the constraint 'a: 'b only involves lifetime parameters (instead of anonymous lifetimes). Lifetime constraints form part of a function's contract; adding one is an API breaking change. The compiler can infer some lifetime constraints based on the types used in the function signature, but it will never infer constraints that arise only from the function's implementation (otherwise an implementation change could silently cause an API breaking change). Explicitly adding where 'a: 'b to the function signature makes the error go away (though it makes the function more restrictive, i.e. some calls that were valid without the constraint become invalid with the constraint):

fn get_x<'a, 'b>(a: &'a Foo, b: &'b Foo) -> &'b str
where
    'a: 'b,
{
    let mut bar = &a.x;
    bar = &b.x;
    bar
}

I don't think it's correct to assume that bar is assigned lifetime 'a based on the initial assignment.

Rather, bar is assigned lifetime 'b based on the return type. That's why it doesn't complain about assigning something from b, but it does complain about assigning something from a.

There's a few things you can do to verify a bit more what's going on here.

First, if you promise that a lives at least as long as b, via fn get_x<'a: 'b, 'b>, the whole program compiles without problem.

Second, to see that the issue is based on type, check out this version of your code: Playground

struct Foo {
    x: String,
}

fn get_x<'a, 'b>(a: &'a Foo, b: &'b Foo) -> &'b str {
    let mut bar: &'_ String = &a.x; // Alternatively try with 'a, 'b or '_ for the lifetime parameter
    bar = &b.x;
    &b.x
}

Here we return something that's definitely correct for the lifetime, and just use bar to play with explicitly specifying the lifetime parameter. The version I posted compiles, because here the borrow checker just finds a lifetime that encompasses both 'a and 'b. But if we replace the '_ with either 'a or 'b it won't compile because now we have a lifetime mismatch when we assign to the same variable from different parameters.

Related