How do I prevent impl Trait moved into function from being dropped while still borrowed?

Viewed 58

I'd like to define a trait Holder that can iterate some through the items method. I don't want to return a trait object from this method because I'd like to stick with static dispatch and stack allocation if possible. The system I have works fine, but fails in a sort of surprising situation.

Here's the code:

pub trait Holder<'a, N: 'a> {
    type Items: Iterator<Item=&'a N>;

    fn items(&'a self) -> Self::Items;
}

struct Impl<N> {
    items: Vec<N>
}

impl<'a, N: 'a> Holder<'a, N> for Impl<N> {
    type Items = std::slice::Iter<'a, N>;

    fn items(&'a self) -> Self::Items {
        self.items.iter()
    }
}

fn use_holder<'a, N: 'a>(holder: impl Holder<'a, N>) {
    // COMPILE ERROR
    for item in holder.items() {

    }
}

Here's the error:

error[E0309]: the parameter type `impl Holder<'a, N>` may not live long enough
  --> src/graph/test.rs:20:17
   |
19 | fn use_holder<'a, N: 'a>(holder: impl Holder<'a, N>) {
   |                                  ------------------ help: consider adding an explicit lifetime bound...: `impl Holder<'a, N> + 'a`
20 |     for item in holder.items() {
   |                 ^^^^^^
   |
note: ...so that the type `impl Holder<'a, N>` is not borrowed for too long
  --> src/graph/test.rs:20:17
   |
20 |     for item in holder.items() {
   |                 ^^^^^^

error[E0309]: the parameter type `impl Holder<'a, N>` may not live long enough
  --> src/graph/test.rs:20:24
   |
19 | fn use_holder<'a, N: 'a>(holder: impl Holder<'a, N>) {
   |                                  ------------------ help: consider adding an explicit lifetime bound...: `impl Holder<'a, N> + 'a`
20 |     for item in holder.items() {
   |                        ^^^^^
   |
note: ...so that the reference type `&'a impl Holder<'a, N>` does not outlive the data it points at
  --> src/graph/test.rs:20:24
   |
20 |     for item in holder.items() {
   |                        ^^^^^

error: aborting due to 2 previous errors; 4 warnings emitted

For more information about this error, try `rustc --explain E0309`.

I follow the compiler's advice and add the explicit lifetime:

pub trait Holder<'a, N: 'a> {
    type Items: Iterator<Item=&'a N>;

    fn items(&'a self) -> Self::Items;
}

struct Impl<N> {
    items: Vec<N>
}

impl<'a, N: 'a> Holder<'a, N> for Impl<N> {
    type Items = std::slice::Iter<'a, N>;

    fn items(&'a self) -> Self::Items {
        self.items.iter()
    }
}

fn use_holder<'a, N: 'a>(holder: impl Holder<'a, N> + 'a) {
    for item in holder.items() {

    }
}

Which gives the error:

error[E0597]: `holder` does not live long enough
  --> src/graph/test.rs:20:17
   |
19 | fn use_holder<'a, N: 'a>(holder: impl Holder<'a, N> + 'a) {
   |               -- lifetime `'a` defined here
20 |     for item in holder.items() {
   |                 ^^^^^^--------
   |                 |
   |                 borrowed value does not live long enough
   |                 argument requires that `holder` is borrowed for `'a`
...
23 | }
   | - `holder` dropped here while still borrowed

I've seen this question and several other tangentially related to this problem, but can't understand how to move forward.

Other than returning a trait object from Hoder#items, what are my options to get the loop in use_hodler working?

1 Answers

This was actually very difficult - lifetimes and associated types appears to be one of those weird corner cases in Rust that is not (yet) well supported. I'd be lying if I said I properly understood the reasons for the error, however I've managed to get this to work for this specific example using Higher Ranked Trait Bounds.

You only need to change one line:

fn use_holder<N>(holder: impl for<'a> Holder<'a, N>) {

Note that the lifetime parameter has moved to the for<'a>. What this means is that the holder parameter implements Holder<'a, N> for every possible lifetime 'a, rather than for a specific 'a that lives at least as long as the function.

You'd need to use for<'a> ... most of the places you use this trait, so it will become messy quite quickly. And higher ranked trait bounds is not a commonly used feature: It's pretty niche and has been elided for the most common cases, so it's not going to help with legibility or maintainability.

I'd recommend changing your architecture to avoid this construction if at all possible.

Related