Trait object with multiple lifetimes

Hi, I've been struggling with lifetimes for a while, I created the minimal code example that reproduces the issue I have in my real application. Please advise how to make this work.

struct OneString {
    s: String,
}
trait TwoStrMaker<'a, 'b> {
    fn make(&'a self, s: &'b str) -> TwoStr<'a, 'b>;
}
impl<'a, 'b> TwoStrMaker<'a, 'b> for OneString {
    fn make(&'a self, s: &'b str) -> TwoStr<'a, 'b> {
        TwoStr {
            onestr: &self.s,
            oneanotherstr: s,
        }
    }
}
type Boxed<'a, 'b> = Box<dyn TwoStrMaker<'a, 'b>>;

struct OneAnotherString {
    s: String,
}
impl OneAnotherString {
    fn combine<'a, 'b>(&'a self, twostrmaker: &'b Boxed<'b, 'a>) -> TwoStr<'b, 'a> {
        twostrmaker.make(&self.s)
    }
}

#[derive(Debug)]
#[allow(dead_code)]
struct TwoStr<'a, 'b> {
    onestr: &'a str,
    oneanotherstr: &'b str,
}

fn main() {
    // this is important that `twostrmaker` is a boxed trait object
    // in the real application I have a Vec of varios such objects
    let twostrmaker: Boxed<'_, '_> = Box::new(OneString {
        s: "foo".to_string(),
    });
    let oneanotherstring = OneAnotherString {
        s: "bar".to_string(),
    };

    let twostr = oneanotherstring.combine(&twostrmaker);

    dbg!(twostr);
}

wich gives an error

error[E0597]: `oneanotherstring` does not live long enough
  --> src/main.rs:43:18
   |
43 |     let twostr = oneanotherstring.combine(&twostrmaker);
   |                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ borrowed value does not live long enough
...
46 | }
   | -
   | |
   | `oneanotherstring` dropped here while still borrowed
   | borrow might be used here, when `twostrmaker` is dropped and runs the destructor for type `Box<dyn TwoStrMaker<'_, '_>>`
   |
   = note: values in a scope are dropped in the opposite order they are defined

error[E0597]: `twostrmaker` does not live long enough
  --> src/main.rs:43:43
   |
43 |     let twostr = oneanotherstring.combine(&twostrmaker);
   |                                           ^^^^^^^^^^^^ borrowed value does not live long enough
...
46 | }
   | -
   | |
   | `twostrmaker` dropped here while still borrowed
   | borrow might be used here, when `twostrmaker` is dropped and runs the destructor for type `Box<dyn TwoStrMaker<'_, '_>>`

I can't really understand what holds these borrows, if this is the twostr why isn't it dropped before anything else? Please help

PS: for me this is important that the types remain as they are in this example. I hope this is possible to fix only by providing correct lifetimes and possibly changing the order in which objects are instantiated.

Why do you have lifetimes on your traits? It makes them invariant and I think basically all your lifetimes are getting entangled so there's no valid drop order (every permutation results in something dropping while infested with the lifetime of something else that dropped earlier).

Also this:

type Boxed<'a, 'b> = Box<dyn TwoStrMaker<'a, 'b>>;

is short for this:

type Boxed<'a, 'b> = Box<dyn TwoStrMaker<'a, 'b> + 'static>

and your example implementations are for non-reference holding types and are valid for any combination of 'a and 'b, not some limited set.

If I replace that with:

// Types must implement for all 'a and 'b
type Boxed = Box<dyn for<'a, 'b> TwoStrMaker<'a, 'b>>;

and clean up the errors, it compiles.

Or alternatively (and probably even better), redo your trait without lifetimes:

trait TwoStrMaker {
    fn make<'a, 'b>(&'a self, s: &'b str) -> TwoStr<'a, 'b>;
}

...but I have to admit I don't really understand what you're trying to do.

5 Likes

Hey, @quinedot your answer is overwhelmingly helpful! Not only does it fix my problem but points at the areas for me to learn (I can't remember seeing the Box<dyn for... notation). Also somehow I assumed that having lifetimes on traits is mandatory, didn't realize I can get away only declaring them locally on a method. This is as far as I've gotten after two weeks of writing in Rust =)
In my real app, the counterpart of the OneAnotherString holds a long flat Vec of numeric values. Each TwoStrMaker knows how to take its slice of that Vec and how to add some info about itself to build the TwoStr... I hope that helps but trust me this app makes a lot of sense =)

Thank you!

Ah cool, glad it works with your actual use case. Lifetimes on traits are usually over-confining; mostly on this forum I see it come up because the programmer had some other lifetime problem and the usually-helpful compiler nudged them towards adding more lifetime annotations everywhere so the compiler could make more progress, when really the problem was unworkable or elsewhere.

Because things with lifetimes (i.e. references) in Rust are almost always short-time borrows, it's usually better to leave them unconstrained: "I can work with a borrow of any length". Usually this means just not writing any lifetimes, but when you have lifetime outputs on a function with multiple lifetime inputs, you have to let the compiler know what inputs might end up in what outputs. But yes, this can be done on the method level, like your functions, and doesn't need to be done on your traits.

The compiler tries hard to find set of lifetimes to assign that it can prove is safe. But when you start using type erasure and whatnot, the compiler has to assume the "worst" in terms of what lifetimes infect what, drop orders, and so on; as it happens, a lot of the flexibility the compiler has goes out the window once you put lifetimes on traits. They're useful in certain circumstances, but it's usually a sign of something being a little weird or off.

As for the aliases, dyn Trait lifetimes, and "higher ranked" types like for<'lifetime>... it's pretty obscure stuff really, especially when you're starting out. But it might be better to avoid the type alias on dyn Traits for now. This is more intuition than concrete reasoning, but when I saw

fn combine<'a, 'b>(&'a self, tsm: &'b Boxed<'b, 'a>) -> ... {

which is short for

fn combine<'a, 'b>(&'a self, tsm: &'b Box<dyn ...>) -> ... {

I thought, hmm, that should really be

fn combine<'a, 'b>(&'a self, tsm: &'b dyn ...) -> ... {

Because if you have a foo: Box<dyn ...>, you could always call it like

let two_str = bar.combine(&*foo);

so it's sort of like preferring to take &str as a paramter instead of &String; the former is more flexible. I don't think it was really causing any problems in this case, but that was my intuitive reaction.


Anyway, welcome to the community and congrats on hitting lifetime spaghetti and dyn Trait complications on your second week with Rust! :sweat_smile:

3 Likes

It's not really useful for learning to assume fundamental properties of the language. When in doubt, an easy way to see what's required and what is not is to look at the standard library instead – it's idiomatic code, and of course, most importantly, it compiles.

I can't recall basically even a single, often-used trait from std that has a lifetime on it at all. Default, Clone, Debug, and even AsRef and Deref lack lifetime parameters, and the last two contain a method each with a signature that ties the lifetime of the returned reference to that of self.

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.