Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Assigning different lifetimes to a single variable

Tags:

rust

lifetime

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?

like image 696
Kasra Ferdowsi Avatar asked Nov 15 '21 21:11

Kasra Ferdowsi


People also ask

How do lifetimes work in Rust?

Lifetimes are what the Rust compiler uses to keep track of how long references are valid for. Checking references is one of the borrow checker's main responsibilities. Lifetimes help the borrow checker ensure that you never have invalid references.

What are lifetime parameters rust?

Rust uses lifetime parameters to avoid such potential run-time errors. Since the compiler doesn't know in advance whether the if or the else block will execute, the code above won't compile and an error message will be printed that says, “a lifetime parameter is expected in compare 's signature.”

What is <' A in Rust?

The <> is used to declare lifetimes. This says that bar has one lifetime, 'a. Rust has two main types of strings: &str and String . The &str are called 'string slices' . A string slice has a fixed size, and cannot be mutated.

What is Anonymous lifetime rust?

'_ , the anonymous lifetime Rust 2018 allows you to explicitly mark where a lifetime is elided, for types where this elision might otherwise be unclear.


Video Answer


1 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
}
like image 77
Francis Gagné Avatar answered Sep 17 '22 07:09

Francis Gagné