Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Lifetimes' handling when returning inner reference from a reference passed as an argument to the function

What is the reason the following code compiles fine, despite both the lifetimes 'a and 'b being independent of each other?

struct Foo<'a> {
    i: &'a i32
}

fn func<'a, 'b>(x: &'a Foo<'b>) -> &'b i32 {
    x.i
}

fn main() {}

If I make the reference i in Foo mutable, it gives the following error.

5 | fn func<'a, 'b>(x: &'a Foo<'b>) -> &'b i32 {
  |                    -----------     -------
  |                    |
  |                    this parameter and the return type are declared with different lifetimes...
6 |     x.i
  |     ^^^ ...but data from `x` is returned here

What is the reason it gives the above error?. Does it consider it's ownership over mutable reference and it sees that something (from Foo) is being taken out (with an independent lifetime), which is not possible, hence the error ?

This code (which I thought would pass) fails too:

struct Foo<'a> {
    i: &'a mut i32
}

fn func<'a, 'b: 'a>(x: &'a Foo<'b>) -> &'b i32 {
    x.i
}

fn main() {}

fails with error:

 error[E0623]: lifetime mismatch
 --> src/main.rs:6:5
  |
5 | fn func<'a, 'b: 'a>(x: &'a Foo<'b>) -> &'b i32 {
  |                        -----------
  |                        |
  |                        these two types are declared with different lifetimes...
6 |     x.i
  |     ^^^ ...but data from `x` flows into `x` here

But this one passes:

struct Foo<'a> {
    i: &'a mut i32
}

fn func<'a: 'b, 'b>(x: &'a Foo<'b>) -> &'b i32 {
    x.i
}

fn main() {}

This seems a bit counter-intuitive to me. Here, the outer lifetime ('a) may outlive the inner lifetime ('b). Why is this not an error?

like image 329
soupybionics Avatar asked Mar 06 '23 11:03

soupybionics


2 Answers

What is the reason the following code compiles fine, despite both the lifetimes 'a and 'b being independent of each other?

fn func<'a, 'b>(x: &'a Foo<'b>) -> &'b i32 {
    x.i
}

The reason is that they are not independent of each other.

The type &'a Foo<'b> would be impossible if 'a outlived 'b. So, implicitly the Rust borrow-checker is inferring that you must have intended that 'b: 'a ('b outlives 'a). So the code above is semantically the same as this:

fn func<'a, 'b: 'a>(x: &'a Foo<'b>) -> &'b i32 {
    x.i
}

If I make the reference i in Foo mutable, it gives the following error.

5 | fn func<'a, 'b>(x: &'a Foo<'b>) -> &'b i32 {
  |                    -----------     -------
  |                    |
  |                    this parameter and the return type are declared with different lifetimes...
6 |     x.i
  |     ^^^ ...but data from `x` is returned here

What is the reason it gives the above error?

When you pass around an immutable reference, it gets copied. In the above example, that means that the &'b i32 can be moved around by itself, and its liveness is not tied to where you got it from. This copied reference always points back to the original address of the data. And that's why the first example works - even if x was dropped, the original reference would still be valid.

When you pass around a mutable reference, it gets moved. A consequence of that is this case is that the reference is still "through" the variable x. If this wasn't the case, x's mutable reference to the contents of Foo could be live at the same time as this new immutable reference - which is not allowed. So, in this case the reference cannot outlive 'a - or put another way: 'a: 'b.

But didn't we already say 'b: 'a? The only conclusion here is that 'a and 'b must be the same lifetime, which is what your previous error message was demanding.

like image 114
Peter Hall Avatar answered Mar 12 '23 07:03

Peter Hall


What is the reason it gives the above error?. Does it consider it's ownership over mutable reference and it sees that something (from Foo) is being taken out (with an independent lifetime), which is not possible, hence the error ?

The mutable borrow indeed cannot be moved out of Foo since mutable borrows are not Copy. It is implicitly immutably reborrowed:

fn func <'a, 'b> (x:&'a Foo<'b>) -> &'b i32 {
    &*x.i
}

However, this is not the source of the problem. First, here is a summary of the four versions of func (struct Foo has no relevance in my explanation):

version 1 (compiles):

fn func<'a, 'b>(x: &'a &'b i32) -> &'b i32 {
    x
}

version 2 (fails to compile):

fn func<'a, 'b>(x: &'a &'b mut i32) -> &'b i32 {
    x
}

version 3 (fails to compile):

fn func<'a, 'b: 'a>(x: &'a &'b mut i32) -> &'b i32 {
    x
}

version 4 (compiles):

fn func<'a: 'b, 'b>(x: &'a &'b mut i32) -> &'b i32 {
    x
}

Version 2 and 3 fail, because they violate the no-aliasing rule which forbids to have a mutable reference and an immutable reference to a resource at the same time. In both versions 'b may strictly outlive 'a. Therefore, &'b mut i32 and &'b i32 could coexist. Version 1 compiles, because the aliasing rules allow multiple immutable references to a resource at the same time. Therefore, &'b i32 may legally coexist with anothor &'b i32.

At first sight, it looks like version 4 should fail since there are again a mutable borrow and an immutable borrow of the same lifetime. The difference to version 2 and 3 is that this time 'a lives at least as long as 'b due to the requirement 'a: 'b, which implies that 'b may not strictly outlive 'a. As long as lifetime 'a lasts the referenced i32 cannot be mutably borrowed a second time (mutable references are not Copy) - the i32 is already mutably borrowed for the func call.

Here is an example demonstrating, how version 2 and 3 could lead to undefined behavior:

fn func<'a, 'b: 'a>(x: &'a &'b mut String) -> &'b str {
    unsafe { std::mem::transmute(&**x as &str) } // force compilation
}

fn main() {
    let mut s = String::from("s");
    let mutref_s = &mut s;
    let ref_s = {
        let ref_mutref_s = &mutref_s;
        func(ref_mutref_s)
    };
    // use the mutable reference to invalidate the string slice       
    mutref_s.clear();
    mutref_s.shrink_to_fit();
    // use the invalidated string slice
    println!("{:?}", ref_s);
}

Swapping version 3 with version 4 shows how the in this case still active outer immutable borrow prevents the second mutable borrow. Lifetime 'a on the outer immutable borrow is forced by the new requirement 'a: 'b to be expanded to be equal to lifetime 'b:

error[E0502]: cannot borrow `*mutref_s` as mutable because `mutref_s` is also borrowed as immutable
  --> src/main.rs:20:5
   |
17 |         let ref_mutref_s = &mutref_s;
   |                             -------- immutable borrow occurs here
...
20 |     mutref_s.clear();
   |     ^^^^^^^^ mutable borrow occurs here
...
23 | }
   | - immutable borrow ends here
like image 38
Calculator Avatar answered Mar 12 '23 06:03

Calculator