Home > Net >  Constraining lifetimes of a borrow of an impl Trait object
Constraining lifetimes of a borrow of an impl Trait object

Time:04-18

Consider the following code:

trait MyTrait<'a, 'b: 'a> {
    fn f(&'b mut self) -> &'a str;
}

struct MyStruct {
    my_string: String,
}

impl<'a, 'b: 'a> MyTrait<'a, 'b> for MyStruct {
    fn f(&'b mut self) -> &'a str {
        &self.my_string
    }
}

fn foo<'a, 'b: 'a>(s: &'b mut impl MyTrait<'a, 'b>) {
    bar(s);
    bar(s); // ERROR
}

fn foo2<'a>(s: &mut MyStruct) {
    bar(s);
    bar(s);
}

fn bar<'a, 'b: 'a>(s: &'b mut impl MyTrait<'a, 'b>) {
    let x = s.f();
}

The trait MyTrait defines a function that returns a str reference. Its lifetime can be interpreted as "the object that returned the str reference will live at least as long as that reference.".

bar is a function that accepts a mutable reference to a struct that implements MyTrait and calls f. The functions foo and foo2 are identical with the only difference being that foo2 accepts a specific type while foo accepts any concrete type that implements MyTrait.

Since foo2 compiles, I expected foo to compile as well but it didn't. Here is the error message:

error[E0499]: cannot borrow `*s` as mutable more than once at a time
  --> src\bin\main2.rs:17:6
   |
15 | fn foo<'a, 'b: 'a>(s: &'b mut impl MyTrait<'a, 'b>) {
   |            -- lifetime `'b` defined here
16 |     bar(s);
   |     ------
   |     |   |
   |     |   first mutable borrow occurs here
   |     argument requires that `*s` is borrowed for `'b`
17 |     bar(s); // ERROR
   |         ^ second mutable borrow occurs here

From what I understand, here is what is going on: MyTrait forces the object that implements it live for the lifetime 'b. The first call to bar borrows s for the lifetime 'b, meaning that the borrow won't drop at the end of bar like a regular borrow, but will drop only at the end of 'b. foo and bar have the same function signature and therefore the lifetimes specified will be identical. This means that when bar mutably borrows s, this borrow will only go out of scope at then end of 'b - at the end of foo. The second bar call will follow the same logic, however since there already exists a mutable borrow, the compiler won't allow another one to be created.

Initially I thought a solution was to rewrite the signature of bar as:

fn bar<'a, 'b: 'a, 'c: 'b>(s: &'b mut impl MyTrait<'a, 'c>)

Hoping the compiler would interpret it as: "'b lives only for the duration of the function, 'a, is even further constrained, and the object itself *s lives for the lifetime 'c which outlives the function". However it resulted in the error:

error[E0495]: cannot infer an appropriate lifetime for autoref due to conflicting requirements

Is it possible to constrain the borrow in bar to only live for the duration of bar?

CodePudding user response:

I'm not sure how you got to the code you posted, but I find the best rule of thumb is to start out with no explicit lifetime parameters, and only add them when the compiler says it needs them.

In your case, the compiler needs no lifetime parameters, it's perfectly happy to figure them all out itself:

trait MyTrait {
    fn f(&mut self) -> &str;
}

struct MyStruct {
    my_string: String,
}

impl MyTrait for MyStruct {
    fn f(&mut self) -> &str {
        &self.my_string
    }
}

fn foo(s: &mut impl MyTrait) {
    bar(s);
    bar(s);
}

fn foo2(s: &mut MyStruct) {
    bar(s);
    bar(s);
}

fn bar(s: &mut impl MyTrait) {
    let x = s.f();
}

In particular, for any function taking one reference and returning a reference, the compiler can work out that the returned reference must be from the argument, so no explicit bounds are needed. What it's actually doing (AFAIK) is basically inserting these lifetimes:

trait MyTrait {
    fn f<'a>(&'a mut self) -> &'a str;
}

impl MyTrait for MyStruct {
    fn f<'a>(&'a mut self) -> &'a str {
        &self.my_string
    }
}

Note how (1), the lifetime is on the function not on the trait. This is another good rule of thumb when adding lifetime parameters, always put them in the most specific scope possible. Also (2), there's no need for two parameters.


Edit:

As far as understanding why it didn't like your version specifically, I think it's basically because the lifetime is defined on the trait rather than on the function. The lifetime 'b on MyTrait in particular is a feature of the type, not of any particular code path. For all the compiler knows, for a generic MyTrait<'a, 'b> those lifetimes could be tied to something else entirely elsewhere in the code, it has no way of knowing that 'b cannot outlive your call to foo because it has to be generic over all possible 'bs.

But in foo2, the compiler can see that all of the lifetimes of MyStruct, it can collapse those lifetimes to the concrete case it has and knows that it only needs to borrow s for the duration of bar.

  • Related