Traits and implementations vs. borrowing and lifetimes


#1

Good day!

I am struggling to understand the difference between an argument of the concrete type implementing a trait vs. an argument of type bound by the same trait which implications on lifetime and borrowing.

The original code:

#![allow(dead_code,unused_imports,unused_variables)]

trait Trait<'a> {
    fn m(&self, options: &'a mut i32) -> usize { 0 }
}

struct Impl;

impl<'a> Trait<'a> for Impl {}

struct S { f: i32 }

impl S {
    fn m<'a, F: Trait<'a>>(&mut self, f: Impl, f2: F) -> &mut i32 {
        f2.m(&mut self.f);
        &mut self.f
    }
}

This fails to compile:

src/lib.rs:15:14: 15:25 error: cannot infer an appropriate lifetime for borrow expression due to conflicting requirements
src/lib.rs:15         f2.m(&mut self.f);
                           ^~~~~~~~~~~
src/lib.rs:14:5: 17:6 help: consider using an explicit lifetime parameter as shown: fn m<'a, F: Trait<'a>>(&'a mut self, f: Impl, f2: F) -> &mut i32
src/lib.rs:14     fn m<'a, F: Trait<'a>>(&mut self, f: Impl, f2: F) -> &mut i32 {
src/lib.rs:15         f2.m(&mut self.f);
src/lib.rs:16         &mut self.f
src/lib.rs:17     }

However if I change f2.m to f.m, everything works fine.

Now let’s follow the advice of the compiler and add lifetime marker:

#![allow(dead_code,unused_imports,unused_variables)]

trait Trait<'a> {
    fn m(&self, options: &'a mut i32) -> usize { 0 }
}

struct Impl;

impl<'a> Trait<'a> for Impl {}

struct S { f: i32 }

impl S {
    fn m<'a, F: Trait<'a>>(&'a mut self, f: Impl, f2: F) -> &mut i32 {
        f2.m(&mut self.f);
        &mut self.f
    }
}

Now again f2 variant fails to compile:

src/lib.rs:16:14: 16:20 error: cannot borrow `self.f` as mutable more than once at a time
src/lib.rs:16         &mut self.f
                           ^~~~~~
src/lib.rs:15:19: 15:25 note: previous borrow of `self.f` occurs here; the mutable borrow prevents subsequent moves, borrows, or modification of `self.f` until the borrow ends
src/lib.rs:15         f2.m(&mut self.f);
                                ^~~~~~
src/lib.rs:17:6: 17:6 note: previous borrow ends here
src/lib.rs:14     fn m<'a, F: Trait<'a>>(&'a mut self, f: Impl, f2: F) -> &mut i32 {
src/lib.rs:15         f2.m(&mut self.f);
src/lib.rs:16         &mut self.f
src/lib.rs:17     }

And f variant again compiles just fine.

May some one, please, explain, what goes on here?

cheers and tia,
anton.


#2

I guess Rust can figure out for f.m() that the borrow is short-lived, since it uses the default uninteresting m(). But since you put that lifetime on the Trait, I can implement it for another type that borrows longer:

struct Impl2<'a> {
    x: std::cell::Cell<&'a i32>,
}

impl<'a> Trait<'a> for Impl2<'a> {
    fn m(&self, options: &'a mut i32) -> usize {
        self.x.set(options); // the reference is mine now!
        0
    }
}

So when faced with your f2 that can be any unknown implementer of Trait<'a>, it has to assume that the argument to m() may be borrowed for the remainder of lifetime 'a.

Your original example works if you just drop all of the lifetime annotations. Then the borrowed reference into m() lasts only for the function call.


#3

Thank you very much for your reply!

The code above won’t compile: Cell requires T: Copy which is not the case and I found no way to make it so. Otherwise mutablilty of self is required.

I still not sure that Rust is allowed to look into the definition of Impl and I am not sure it’s possible to write implementation that will steal the ref, but I may miss some obvious solution. Maybe some statics should be around.

Again, thank you for the answer,


#4

Not sure what you mean – for my Cell<&i32> it should be fine, as any & implements Copy. Now, &mut doesn’t have Copy, but you can use RefCell for that. Try here in the playground: http://is.gd/WtRGAK

You can see in the playground that I definitely stole the reference, even mutable. I modified the value in Impl3::drop, and this is reflected in the original.

As for Rust looking into the definition, to see that the default Trait::m() doesn’t steal the reference — I don’t know. The more I think about this, the more it surprises me too. I hope someone else can explain this…


#5

Thanks a lot again! Yes, I was wrong with the reference, most probably lost ampersand somehow.

And yes, I agree it still surprises me how Rust allows Trait::m variant.