In Rust, when boxing a value passed as a generic argument, why is a `'static` lifetime bound required?
Asked Answered
S

1

6

I'm trying to write a constructor function which takes a generic value implementing some trait by argument, and then boxes it (the point is to then initialize something with these boxes, but the following example is simplified):

struct Struct {}

trait Trait {}

impl Trait for Struct {}

fn f(arg: impl Trait) -> Box<dyn Trait> {
    Box::new(arg)
}

fn main() {
    let x = Struct {};
    f(x);
}

Here, the compiler whines that arg might not live long enough. This makes perfect sense to me, because the only requirement on arg is impl Trait, meaning it might in fact be a reference which implement the trait, in which case it cannot be boxed safely.

What confuses me is the following solution, adding a 'static bound:

fn f(arg: impl Trait + 'static) -> Box<dyn Trait> {
    Box::new(arg)
}

Now, of course it works, but in principle we are now constrained to passing in values with a 'static lifetime. Yet, the compiler lets me pass in x without a problem.

Here are my questions, more specifically:

  1. Doesn't x have a lifetime, residing on the stack? Why is it possible to pass it to f when arg has a 'static lifetime bound? Do these bounds only concern the lifetime of references?
  2. Is this solution valid in general, or will I face cases where the compiler will refuse my stack-allocated arguments?
  3. Is there a nicer way of saying "any type that impls Trait where the type is not a reference?

Note that 1) is mostly answered by Why does Rust require a `'static` lifetime for this variable? but I am left confused as to if this is the idiomatic way of boxing arguments.


EDIT:

Now that I understand better, I am left wondering what is the idiomatic solution to fix the compiler error in the case of populating a struct:

struct OtherStruct {
    x: Box<dyn Trait>,
}

impl OtherStruct {
    fn new(arg: impl Trait) -> Self { // Does not compile
        Self { x: Box::new(arg) }
    }
}

The only solutions I see so far are 1) adding a lifetime parameter to OtherStruct (not so great), adding a 'static lifetime bound to arg (I'm not sure if this is OK?)

Shut answered 10/6, 2021 at 20:25 Comment(4)
Here's a couple good sources of info on this: rust by example section, lifetime misconceptionsBopp
The meanings of &'static arg vs. arg: 'static are not the same. The first is a lifetime required of a reference, and the second is a bound. In the second case, arg thus bounded doesn't itself have to be static, it just has to either own all its data, or if its struct has references, they have to have a 'static lifetime; but arg itself doesn't need to be static.Bopp
However... if you were to implement impl Trait for &Struct {}.. then that would permit you to pass a reference to Trait objects to function f() like so: f(&x). In this specific case, x will have to have a static lifetime . This probably confuses the issue, but worth mentioning anyway.Bopp
That makes sense. So recapitulating, owned values can be moved into any scope, giving it essentially any lifetime whatsoever, so the 'static constraint don't impede them. I guess what was confusing me is that the bound describes the lifetime of a value after it is moved into the function, while for some reason I expected it to describe the lifetime it has before it.Shut
N
4

You have a couple misconceptions here.

Doesn't x have a lifetime, residing on the stack? Why is it possible to pass it to f when arg has a 'static lifetime bound? Do these bounds only concern the lifetime of references?

When you are doing f(x), because you don't take a reference to x, you are moving the value of x into the function, which shifts its lifetime. If you tried to use x again after calling f(x), Rust will fail to compile your code and tell you this.

As for the + 'static bound... Box<dyn Trait> is shorthand for Box<dyn Trait + 'static>, which is why the compiler was giving you an error. The type system needs to know the lifetime of the implementation inside of the box so that it can check it. You could give the box a different lifetime, if you wish, explicitly:

fn f<'a>(arg: impl Trait + 'a) -> Box<dyn Trait + 'a> {
    Box::new(arg)
}
Nichollenicholls answered 10/6, 2021 at 20:53 Comment(5)
Sorry, maybe I wasn't so clear, but the code: fn f(arg: impl Trait + 'static) -> Box<dyn Trait> { Box::new(arg) } does compile.Shut
Yes, because Box<dyn Trait> is shorthand for Box<dyn Trait + 'static>, so the lifetimes match up. But it also requires arg to be 'static, which is restrictive. My alteration does not.Nichollenicholls
I see, thanks! I didn't even realize you can add a bound the trait in the return value like that. I must ask, in the case of constructor methods, what solution is considered idiomatic? It is not so easy to add a bound "inside the box" in that case. (I have edited the question with an example, since apparently stackoverflow does not allow for multiline code in comments)Shut
Just wondering, why are the default lifetime (when it's not annotated explicitly) of impl Trait and that of Box<dyn Trait> different? That is, the former will default to impl Trait + 'a, while the latter will default to Box<dyn Trait + 'static>.Desai
@Desai I believe it's because the Box contents must outlive the Box (live at least as long as the box itself), and the only way to indicate this in a general case is 'static.Trifoliate

© 2022 - 2024 — McMap. All rights reserved.