Confusing error E505

Good day!

Explain please why error E505 occurs in this example.

struct Data<'a>(&'a u8);

trait Bar {
    fn bar<'a>(&'a self, _: &mut Data<'a>) {}
}

struct Foo;
impl Bar for Foo {}

fn foo() {
    let x = Foo;
    let ref mut y = Data(&5);
    x.bar(y);
    x;
    y.0;
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0505]: cannot move out of `x` because it is borrowed
  --> src/lib.rs:14:5
   |
13 |     x.bar(y);
   |     - borrow of `x` occurs here
14 |     x;
   |     ^ move out of `x` occurs here
15 |     y.0;
   |     --- borrow later used here

Why y.0 is considered by compiler as a use of borrowed x?

15 |     y.0;
   |     --- borrow later used here

The signature of bar is responsible:

    fn bar<'a>(&'a self, _: &mut Data<'a>) {}

Note that it requires the borrow of self to be the same as the lifetime parameter of Data, which is also the lifetime of the element within Data. Within foo we have self = x. The later use of y.0 requires that element to be alive up to that point which then requires the lifetime paramter of Data to extend to that point which then also requires x to still be borrowed. Thus, the borrow of x in foo has the same lifetime as the parameter.

1 Like

Basically the thing is that the signature of bar allows y to contain a borrow of x. That the implementation doesn't actually do this is not important: the signature determines if the borrows are allowed.

Consider another example.
If we apply the same logic then lifetime of y is also must be equal to lifetime of x. And compilation must fail by same reason. Isn't it?
But it compiles successfully...


trait Bar {
    fn bar<'a>(&'a self, _: &'a mut u8) {}
}

struct Foo;
impl Bar for Foo {}

fn foo() {
    let x = Foo;
    let ref mut y = 5;
    x.bar(y);
    x;
    y;
}

(Playground)

15 |     y.0;
   |     --- borrow later used here

This error explanation tells that borrow of x IS (not potentially CAN) used here.
If so may be better to refrase this wrong explanation...

Not quite, nothing stops the lifetime 'a to end immediately after the function execution here. Note that references can be reborrowed for a shorter lifetime themselves. Thus the x.bar(y) actually creates a temporary reference to the y that lives short enough to end before x;.

Well x is borrowed here. The borrow was not used for anything, but the borrow did happen: as I said, y is allowed to store a reference into x, and this ability is what makes x borrowed.

But error explanation tells that borrow is used.

Yes, I meant that bar did not use it. The error says it was used somewhere else. The thing is that the signature of bar allows bar to use the borrow of x, and because of this, x becomes borrowed.

I'm so sorry for so many misunderstadings. I'm newbe in Rust.
But I still don't understand.

15 | y.0; |
     --- borrow later used here

I understand this words as: during execution of y.0; there is a use of a borrow of x.
You tell that x is borrowed at this point. I understand this.
But y don't actually content borrow of x. How borrow of x is actually used by y.0?
You say that y.0 potentially allow to contain a borrow of x. But in this case error explanation can be --- borrow can potentially be used here.

I can try to be more precise in my wording when I get back to a computer.

Let's just go over what it means for something to be borrowed again, because this is touching on some subtle details. There's also the fact that a reference to something is sometimes called "a borrow of that thing", but in the following I will try to consistently use "reference" to talk about references.

Let's define what I mean when I say "a borrow". It consists of two things:

  1. Some variable that has become borrowed.
  2. A section of code in which the variable is borrowed. This is called the lifetime of the borrow.

Now, creating a borrow also gives you a reference to the variable you borrowed, but this reference is ultimately a separate thing from the borrow itself. They are however still tied closely together: this reference is only valid within the borrow, and the compiler enforces this by making the lifetime of the borrow part of the type of this reference. That said, you are allowed to throw away a reference before the lifetime ends.

Now the question becomes: What influences the size of the lifetime? There are a few things:

  1. Using the borrowed variable inside the lifetime of the borrow is only allowed if it is consistent with the type of borrow.
  2. Using something (e.g. a reference, but could also be Data<'a>) annotated with the lifetime is only allowed inside the lifetime.

The compiler will try to figure out what the lifetime should be by looking at uses of the two kinds above and using them to create a list of constraints on the lifetime. Note that immutable and mutable borrows have different requirements: An immutable borrow is allowed to overlap with another immutable borrow of the same variable, but mutable borrows may not overlap with any other borrow of the same variable.

For example:

let mut x = "abc".to_string();
let y = &mut x; // a mutable borrow of x starts here
println!("{}", x); // x is used here

In this case it knows that the borrow of x starts on the second line, and it knows that since x was used on line three, the mutable borrow cannot be active on line three. There exists a lifetime consistent with these constraints: The lifetime that starts and ends on line two. However in this case:

let mut x = "abc".to_string();
let y = &mut x; // a borrow of x starts here
println!("{}", x); // x is used here
println!("{}", y); // x must still be borrowed here

In this case we have the constraint that the lifetime of the borrow must have finished at line three, but the use of y on line four means that, due to the fact that the lifetime of the borrow is part of the type of y, that the lifetime must still be valid on line four. There is no possible choice of lifetime that satisfies both constraints, so you get a compile error.

In this example:

let x = "abc".to_string();
let y = &x; // a borrow of x starts here
println!("{}", x); // x is used here
println!("{}", y); // but the borrow is still active here

In this case we have no compile error, but this is because both uses of x only borrow x immutably, and immutable borrows are allowed to overlap.

Let's go back to the situation in question. I will first address the version that compiles:

trait Bar {
    fn bar<'a>(&'a self, _: &'a mut u8) {}
}

struct Foo;
impl Bar for Foo {}

fn foo() {
    let x = Foo;
    let ref mut y = 5;
    x.bar(y); // Both x and y are borrowed in order to perform this call
    x;
    y;
}

Well let's see what constraints apply on the two borrows that occurs when we call bar. The first thing is that bar requires that the lifetimes on the two references must be equal. Now, you may notice that y already has type &'l mut u8, which has an lifetime 'l that is part of the type. However, this does not necessarily mean that the lifetime of the mutable reference that bar receives is 'l, because the compiler might implicitly reborrow the mutable reference.

Reborrowing a mutable reference means that the reference itself is now borrowed. Note that the mutable reference was itself originally created by borrowing some third variable, so you actually have several borrows on top of each other, each with shorter and shorter lifetimes.

So by reborrowing y and creating something with a very short lifetime, and additionally doing the same to x, we can produce two references of a very short lifetime, and this common lifetime is so short that it ends before the use of x and y that follows.

Now for the version that does not compile. We have a function with this signature:

fn bar<'a, 'b>(&'a self, _: &'b mut Data<'a>)

Note that I have inserted an extra lifetime which was elided in the original version. In this case the lifetimes that must be equal is the one on x, and the one that is a generic parameter to Data.

fn foo() {
    let x = Foo;
    let ref mut y = Data(&5);
    x.bar(y);
    x;
    y.0;
}

The generic parameter refers to the lifetime found on the immutable reference &5. In this case, since this immutable reference is stored inside y, it must obviously be valid until y no longer exists. The thing is that reborrowing does not change 'a. It changes 'b. So since the compiler does not have a mechanism of making the lifetime on &5 shorter, and since it must be equal to the lifetime on the reference to x created in order to call bar, the lifetime on this reference to x must extend until y no longer exists.

As for the fact that bar does not actually use the references, well that doesn't matter. You are allowed to throw away the reference before the lifetime ends.

Note that had bar taken an immutable reference to Data instead, it would compile. This is because immutable and mutable references are different, and you can actually shorten the inner 'a lifetime when the reference is immutable. You can read more about why here.

4 Likes

It's really brilliant explanation! Thank you!
Now I think I have a full understanding.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.