Infinite borrowing when passing borrowed value to Box<dyn Trait>

Hi :slight_smile:

I'm writing a code below, and got an Error....

The problem looks like that I am passing an iterator iter_borrowed_from that shares the lifetime with the variable s to fn my_method of Box<dyn MyTrait>.
The issue arises because my_method might store iter_borrowed_from somewhere. So It is not released even if my_method ends.

I was wondering, Is there any way to tell the compiler my_method is not storing iter_borrowed_from ?

Code:

use std::boxed::Box;

trait MyTrait<It> {
    fn my_method(&self, it: It);
}

struct X {/* private fields */}

impl<It> MyTrait<It> for X {
    fn my_method(&self, it: It) {
        // private body
    }
}

fn main() {
    let dyn_trait: Box<dyn MyTrait<_>> = Box::new(X {});

    let s: String = String::new();
    let iter_borrowed_from = s.chars();

    dyn_trait.my_method(iter_borrowed_from);

    // iter_borrowed_from Drops here <-- ERROR? iter_borrowed_from is still being borrowed in dyn_trait.my_method
    // s Drops here
    // dyn_trait Drops here
}

Error:

error[E0597]: `s` does not live long enough
  --> examples/calculator/src/main.rs:19:30
   |
18 |     let s: String = String::new();
   |         - binding `s` declared here
19 |     let iter_borrowed_from = s.chars();
   |                              ^ borrowed value does not live long enough
...
26 | }
   | -
   | |
   | `s` dropped here while still borrowed
   | borrow might be used here, when `dyn_trait` is dropped and runs the destructor for type `Box<dyn MyTrait<Chars<'_>>>`
   |
   = note: values in a scope are dropped in the opposite order they are defined

By the way, I was trying to implement 'virtual class wrapper' in C++...

The problem is that you're naming the iterator type as an argument to the trait, which in turn means that some implementor of MyTrait might try to store the parameter. Because dyn MyTrait<_> has to be able to represent any possible implementor, the compiler has to treat it like it does store the iterator.

One way to get around this is to erase the type of the iterator as well:

use std::boxed::Box;

trait MyTrait<T> {
    fn my_method(&self, it: &mut dyn Iterator<Item=T>);
}

struct X {/* private fields */}

impl<T> MyTrait<T> for X {
    fn my_method(&self, it: &mut dyn Iterator<Item=T>) {
        // private body
    }
}

fn main() {

    let s: String = String::new();
    let dyn_trait: Box<dyn MyTrait<_>> = Box::new(X {});
    let mut iter_borrowed_from = s.chars();

    dyn_trait.my_method(&mut iter_borrowed_from);
}
1 Like

This top part is mainly useful if you like solving lifetime puzzles. Something potentially useful is at the bottom.

The low-level cause (which does prevent this high-level unsoundness) is invariance. Namely, trait parameters are invariant. So in the case of a trait object, the method signature takes

&dyn MyTrait<Chars<'c>>, Chars<'c>
//           ^^^^^^^^^

Because the underlined part is invariant, the lifetime has to be the entirety of the borrow 'c. This is what ties the borrow of s to the type of dyn_trait:

Box<dyn MyTrait<Chars<'c>> + '_>

and the final piece of the puzzle is that trait objects always have a non-trivial drop, so from a low-level borrow checking perspective, there's a use of the borrow of s when dyn_trait goes out of scope.

More generally, if the following three ingredients are a recipe for borrow entanglement:

  1. A method taking different parameters with the same lifetime
  2. Something causing the lifetime to be invariant
  3. A non-trivial drop

The invariance of trait parameters means that a dyn MyTrait<Chars<'c>> is saying, "I can only work with Chars<'a> if 'a is exactly 'c". Contravariance would be "'c or longer", etc.

There is a way to be more general without changing the trait: you can have a higher-ranked bound that says "I can work with Chars<'a> for any 'a". That would be this change:

-    let dyn_trait: Box<dyn MyTrait<_>> = Box::new(X {});
+    let dyn_trait: Box<dyn for<'a> MyTrait<Chars<'a>>> = Box::new(X {});

Now the lifetimes in the method call don't have to be the same.

Whether the implementors you want can meet that bound or not is a different question, so this may or may not be a viable alternative.

In terms of @2e71828 solution, you will end up with the same borrowing situation as the OP if the iterator Item type is something borrowed. The higher-ranked trait object type (commented in the playground) again may or may not be a viable workaround, depending on the MyTrait implemention of the base type.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.