Mutual borrow in the loop with lifetimes


#1

Hi,

I’d like to start with the example that does not work:

pub struct S1;

pub trait T1<'a> {
    fn exec(&mut self, v: &'a [u8]);
}

impl<'a> T1<'a> for S1 {
    fn exec(&mut self, v: &'a [u8]) {
        println!("{:?}", v[0]);
    }
}

pub fn t1v<'a, T: T1<'a>>(vec: &'a[u8], t: &'a mut T) {
    for i in &[0, 1, 2][..] {
        t1(&vec[*i as usize..], t);
    }
}

pub fn t1<'a, T: T1<'a>>(v: &'a [u8], t: &'a mut T) -> Option<()> {
    t.exec(v);
    Some(())
}

fn main() {
    let mut t = S1;
    let vec1 = b"12345";
    t1v(&vec1[..], &mut t);
}



src/test.rs:17:33: 17:34 error: cannot borrow `*t` as mutable more than once at a time [E0499]
src/test.rs:17         t1(&vec[*i as usize..], t);
                                               ^
src/test.rs:17:33: 17:34 help: run `rustc --explain E0499` to see a detailed explanation
src/test.rs:17:33: 17:34 note: previous borrow of `*t` occurs here; the mutable borrow prevents subsequent moves, borrows, or modification of `*t` until the borrow ends
src/test.rs:17         t1(&vec[*i as usize..], t);
                                               ^
src/test.rs:19:2: 19:2 note: previous borrow ends here
src/test.rs:15 pub fn t1v<'a, T: T1<'a>>(vec: &'a[u8], t: &'a mut T) {
src/test.rs:16     for i in &[0, 1, 2][..] {
src/test.rs:17         t1(&vec[*i as usize..], t);
src/test.rs:18     }
src/test.rs:19 }
               ^
error: aborting due to previous error

In my understanding, I failed to explain to compiler that the borrow of t that is originated at t1(…, t); ends right then and there. This is probably because lifetime of t is specified to be any. Is there a way to tell compiler that no matter lifetime the borrow really ends there?
I can work around this problem by excluding lifetime from Trait definition. So the working example looks like this:

pub struct S1;

pub trait T2 {
    fn exec<'a>(&mut self, v: &'a [u16]);
}

impl T2 for S1 {
    fn exec<'a>(&mut self, v: &'a [u16]) {
        println!("{:?}", v[0]);
    }
}

pub fn t2v<'a, T: T2>(vec: &'a[u16], t: &'a mut T) {
    for i in &[0, 1, 2][..] {
        t2(&vec[*i as usize..], t);
    }
}

pub fn t2<'a, T: T2>(v: &'a [u16], t: &'a mut T) -> Option<()> {
    t.exec(v);
    Some(())
}

fn main() {
    let mut t = S1;
    let vec1 = b"12345";
    let vec2 = [1, 2, 3, 4, 5];
    t2v(&vec2[..], &mut t);
}

     Running `target/debug/test`
1
2
3

However my question is regarding the first example:
What is the most “rustian” way to handle multiple mutual borrow with any lifetime in the scope of iteration in rust?
Is it possible without interior mutability?

Thank you.


#2

I am not sure what you are doing here, but the lifetime in the trait is tying the lifetime of the argument ‘v’ to the lifetime of the type parameter T1.

Later in t2 you tie the lifetime of argument ‘t’ to the lifetime of argument ‘v’ which is in turn tied to T1 when you call exec in t2.

The result of this is the lifetime of the borrow in the loop is extended to the lifetime of S1, which is the scope of ‘main’, so as far as I can see the failure is exactly what you have asked for with the lifetimes you have specified.


#3

First of all thanks a lot for taking time to answer my question.
I agree with your explanation, that’s exactly how I was explaining this to myself, however my question is this: Is there a way to give a “hint” to compiler, that the borrow in the call to t1(&vec[*i as usize…], t) ends the moment the call returns? Apology for not very clear question.
Also if, in the first example, we were to replace call t1(&vec[*i as usize…], t) with t.exec(&vec[*i as usize…]) (removing the indirection) the code would compile and work, which is a bit confusing to me, I guess it works because in exec the mutual borrow of self is not bounded by any lifetime.


#4

The answer is not to break one of the lifetime ‘links’. So for example, do not tie the lifetime of ‘v’ to ‘t’:

pub fn t1<'a, T: T1<'a>>(v: &'a [u8], t: & mut T) -> Option<()> {

Why did you want to link the lifetime of ‘v’ to ‘t’ in the first place?


#5

In my first example the trait T1 was parametrized with lifetime 'a, that’s why I had a link in that example.
I agree with you braking the link between lifetimes would work, I showed that in the second example (where T2 was not parametrized with 'a but rather exec was). But I was wondering if I can somehow tell compiler that the borrow is still ok as it doesn’t last beyond the call and technically should be safe.

The reason this whole thing came up was me playing with some ideas while trying to understand Rust generics.
This example was reduced from some code I’ve been toying with where there is a trait defined as:

trait TA<T> {
    exec(&mut self, v: T);
}

later on I was specializing that trait as following:

trait TB<'a>: TA<&'a [u8]> {};

That’s where the lifetime came to be. And then later on I had a concrete implementation of trait TB, and I was trying to work with it and was getting the aforementioned error.


#6

but why do you want:

pub fn t1<'a, T: T1<'a>>(v: &'a [u8], t: &'a mut T) -> Option<()> {

and not (for example)

pub fn t1<'a, 'b, T: T1<'a>>(v: &'a [u8], t: &'b mut T) -> Option<()> {

?


#7

Thank you. I’ll try that, I think that should work, I guess I didn’t realize that reference in my case can and should have completely different lifetime than lifetime parameter of the type it refers to.

I kind of enjoying this learning curve :slight_smile: , lifetimes is an interesting concept in rust, I’ve read some posts/blogs proposing non lexical lifetimes, which would make borrow checker smarter/more forgiving, I believe it was even approved and will be implemented as a part of future rust releases.

BTW, side note, is there a way to specify the relationship between lifetimes? With traits I can say that TA: TB, which effectively means that TB is a subset of TA. I don’t think I can say 'a: 'b, meaning 'b outlives 'a?


#8

My understanding (but I might be wrong) is that the only things with definite lifetimes are stack objects. Lifetime variables are all about tying references to the lifetimes of the things on the stack, and hence eventually to some stack object. The stack always has the property that objects on the top of the stack have shorter lifetimes that objects further down the stack. So if we create some object on the stack, we could tie the lifetime of the reference to this object and that would be safe, but we can also tie the lifetime of the reference to any object on top of the object we want to reference, for example the stack frame of a function. When a function borrows a reference to an argument, the default lifetime is the stack frame of the function it is passed to, which is always going to be less than the lifetime of an object further down the stack, and therefore is always safe, unless you try and return the reference from the function, or there is another thread that may remove the object concurrently.