Managed to make this run by adding lots of `'_`, need help understanding them

From the recommendations in the previous post, I managed to make the code in that post run with better_any. However I ended up adding lots of '_ that I don't understand at all.

use better_any::*;

trait Trait<'a>: Tid<'a> {
    fn do_it(&self) -> Box<dyn Trait<'_> + '_>;
}

#[derive(Clone, Debug, Tid)]
struct A(&'static str);

impl<'a> Trait<'a> for A {
    fn do_it(&self) -> Box<dyn Trait<'_> + '_> {
        println!("A: {}", self.0);
        Box::new(B(self))
    }
}

#[derive(Tid)]
struct B<'a>(&'a A);

impl<'a> Trait<'a> for B<'a> {
    fn do_it(&self) -> Box<dyn Trait<'_> + '_> {
        println!("B: {:?}", self.0);
        Box::new(self.0.clone())
    }
}

fn main() {
    let s = "aaaaa";
    let a: Box<dyn Trait<'_>> = Box::new(A(s));
    let b: Box<dyn Trait<'_>> = a.do_it();
    let a2: Box<dyn Trait<'_>> = b.do_it();
    let b2: Box<dyn Trait<'_>> = a2.do_it();
    b2.do_it();
}

/*
Expected output:
A: aaaaa
B: A("aaaaa")
A: aaaaa
B: A("aaaaa")
*/

What I know for sure is that the &A in B must outlive the B. I actually don't know how to type the trait defs correctly. :frowning: Because if I write this:

trait Trait<'a>: Tid<'a> {
    fn do_it(&self) -> Box<dyn Trait<'a>>;
}

impl<'a> Trait<'a> for A {
    fn do_it(&self) -> Box<dyn Trait<'a>> {
        println!("A: {}", self.0);
        Box::new(B(self))
    }
}

I get this lifetime error:

error: lifetime may not live long enough
  --> src\main.rs:13:9
   |
10 | impl<'a> Trait<'a> for A {
   |      -- lifetime `'a` defined here
11 |     fn do_it(&self) -> Box<dyn Trait<'a>> {
   |              - let's call the lifetime of this reference `'1`
12 |         println!("A: {}", self.0);
13 |         Box::new(B(self))
   |         ^^^^^^^^^^^^^^^^^ method was supposed to return data with lifetime `'a` but it is returning data with lifetime `'1`

If I change it to '_:

trait Trait<'a>: Tid<'a> {
    fn do_it(&self) -> Box<dyn Trait<'_>>;
}

impl<'a> Trait<'a> for A {
    fn do_it(&self) -> Box<dyn Trait<'_>> {
        println!("A: {}", self.0);
        Box::new(B(self))
    }
}

I get error with a help “to declare that the trait object captures data from argument self, you can add an explicit '_ lifetime bound”:

error: lifetime may not live long enough
  --> src\main.rs:13:9
   |
11 |     fn do_it(&self) -> Box<dyn Trait<'_>> {
   |              - let's call the lifetime of this reference `'1`
12 |         println!("A: {}", self.0);
13 |         Box::new(B(self))
   |         ^^^^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'static`
   |
help: to declare that the trait object captures data from argument `self`, you can add an explicit `'_` lifetime bound
   |
11 |     fn do_it(&self) -> Box<dyn Trait<'_> + '_> {
   |                                          ++++

So... that's how I ended up with all the '_s.

Could someone explain them to me?

1 Like

You can just make B own A directly, as you are cloning it anyway aftwerward, the added complexity of lifetimes is not necessary for this.

Actually I've managed to do it without the lifetime hassle using Rcs.

However I still wonder how the '_s work. Like what they deduce to and why this even compiles.

1 Like

You can just do this instead of using Rc, that's what I meant by B owning A:

use downcast_rs::{Downcast, impl_downcast};
use std::fmt::Debug;

trait Trait: Downcast + Debug {
    fn do_it(&self) -> Box<dyn Trait>;
}

impl_downcast!(Trait);

#[derive(Clone, Debug)]
struct A(pub &'static str);

impl Trait for A {
    fn do_it(&self) -> Box<dyn Trait> {
        println!("A does: {}", self.0);

        Box::new(B(self.clone()))
    }
}

#[derive(Debug)]
struct B(pub A);

impl Trait for B {
    fn do_it(&self) -> Box<dyn Trait> {
        println!("B does: {:?}", self.0);

        Box::new(self.0.clone())
    }
}

fn main() {
    let a: Box<dyn Trait> = Box::new(A("it"));
    println!("In downcast: {:?}", a.downcast_ref::<A>().unwrap().0);

    let b = a.do_it();
    println!("In downcast: {:?}", b.downcast_ref::<B>().unwrap().0);

    let a2 = b.do_it();
    println!("In downcast: {:?}", a2.downcast_ref::<A>().unwrap().0);

    let b2 = a2.do_it();
    println!("In downcast: {:?}", b2.downcast_ref::<B>().unwrap().0);

    b2.do_it();

    println!();
    println(
        "Concrete values: {:?}, {:?}, {:?}, {:?}",
        a.downcast_ref::<A>().unwrap(),
        b.downcast_ref::<B>().unwrap(),
        a2.downcast_ref::<A>().unwrap(),
        b2.downcast_ref::<B>().unwrap(),
    );
}

About the part about the two '_ in Box<dyn Trait<'_> + '_>, they serve different purposes. My understanding is:

  1. Trait<'_> - The trait's own generic lifetime parameter
  2. + '_ - The trait object's lifetime bound, this is the critical one (It gets confusing due to the ellision)

When A::do_it(&self) returns Box::new(B(self)), B holds a reference to self. The returned trait object therefore captures borrowed data from &self. Without any lifetime bound, the compiler currently defauts to + 'static, meaning "this box contains no borrowed data" But B borrows from self, so it's not 'static.... which is why you get the error.

The + '_ after dyn Trait<...> declares that the boxed value may hold references tied to &self's lifetime, rather than requiring everything inside to be owned or 'static.

3 Likes

I'm going to go over the same things @consistent-milk12 said, only slower.

The code doesn't need better_any; this is just a question about lifetimes in traits. I'll just be talking about vanilla Rust.

The main thing to be aware of is that in function siguatures, these mean the same thing:

Box<dyn Trait<'a>>
Box<dyn Trait<'a> + 'static>

It's unfortunate that 'static is the default for traits with lifetime parameters, but that's the way it is. That's why you had to add + '_ for the second error.

Now I'll talk about coercion more generally, in case it clears things up for you.


We're dealing with coercions to this type:

Box<dyn Trait<'parameter> + 'dyn_lifetime>

In order to coerce a value of type Box<X> to Box<dyn Trait<'parameter> + 'dyn_lifetime>, the following bound needs to hold:

X: Trait<'parameter> + 'dyn_lifetime

The 'dyn_lifetime exists to make sure you can't use the dyn Trait<'_> after the value you coerced becomes invalid.

Looking at the implementation signatures:

impl<'a> Trait<'a> for A { ... }
impl<'a> Trait<'a> for B<'a> { ... }

Each B<'a> implements Trait<'a> only, whereas A implements Trait<'x> for all lifetimes 'x. A has no lifetimes in its type, which means it meets a 'static bound -- and in fact, that means it meets any lifetime bound.

So Box<A> can coerce to Box<dyn Trait<'x> + 'y> for any two lifetimes 'x and 'y.

How about Box<B<'b>>? B<'b> only implements Trait<'b>. If we ignore variance for a moment, that means that Box<B<'b>> can coerce to Box<dyn Trait<'b> + 'beta> for some lifetimes 'beta. Which lifetimes? The lifetimes such that B<'b>: 'beta. That reduces to 'b: 'beta. So 'beta could be 'b or it could be shorter than 'b. It cannot be greater than 'b.

In practice, like in your implementations, Box<B<'b>> will often be in some covariant position, and it will be able to coerce to a Box<B<'shorter_than_b>> before coercing to Box<dyn Trait<'_> + '_>. So in that situation, Box<B<'b>> can coerce to Box<dyn Trait<'x> + 'y> so long as 'b: 'x and 'b: 'y.

That's how the lifetimes in the trait work with regards to coercion.


Now we can walk through the errors. The implementation for B<'_> coerces a Box<A>, and like we talked about, that's going to work for any lifetimes. So we'll stick to the implementation for A. With your first signature, you had

impl<'a> Trait<'a> for A {
    // I've gotten rid of all lifetime elision; this is the same as
    // fn do_it(&self) -> Box<dyn Trait<'a>>
    fn do_it<'this>(&'this self) -> Box<dyn Trait<'a> + 'static> {
        println!("A: {}", self.0);
        Box::new(B(self))
    }
}

You can create at most a Box<B<'this>> -- you can't make the borrow of *self any longer. But this implementation is supposed to work for all lifetimes 'a -- like when 'a = 'static for one example. The Box<B<'this>> can't coerce to Box<dyn Trait<'a> + '_> when 'a is longer than 'this.

You fixed that part of the problem by changing the signature:

impl<'a> Trait<'a> for A {
    // Same as: fn do_it(&self) -> Box<dyn Trait<'_>>
    //
    //                          This part changed vvvvv
    fn do_it<'this>(&'this self) -> Box<dyn Trait<'this> + 'static> {
        println!("A: {}", self.0);
        Box::new(B(self))
    }
}

Now the problem is the dyn lifetime. B<'this>: 'static doesn't hold. At most, B<'this>: 'this can hold. That's why you had to change the signature to add another '_.

impl<'a> Trait<'a> for A {
    // Same as: fn do_it(&self) -> Box<dyn Trait<'_> + '_>
    //
    //                                   This part changed vvvvv    
    fn do_it<'this>(&'this self) -> Box<dyn Trait<'this> + 'this> {
        println!("A: {}", self.0);
        Box::new(B(self))
    }
}

And now we're back at the version that compiles.

4 Likes