Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

`impl Trait` return type causes wrong lifetime elision

The impl Trait syntax for return types seems to cause the compiler to incorrectly assume that the lifetime of the input argument must match the output in some situations. Consider the function

fn take_by_trait<T: InTrait>(_: T) -> impl OutTrait {}

If the input type contains a lifetime the compiler complains when the output outlives the it even though they are completely independent. This will not happen if the input type is not generic, or if the output is a Box<dyn OutTrait>.

Full code:

trait InTrait {}

struct InStruct<'a> {
    _x: &'a bool, // Just a field with some reference
}

impl<'a> InTrait for InStruct<'a> {}

trait OutTrait {}

impl OutTrait for () {}

fn take_by_type(_: InStruct) -> impl OutTrait {}

fn take_by_trait<T: InTrait>(_: T) -> impl OutTrait {}

fn take_by_trait_output_dyn<T: InTrait>(_: T) -> Box<dyn OutTrait> {
    Box::new(())
}

fn main() {
    let _ = {
        let x = true;
        take_by_trait(InStruct{ _x: &x }) // DOES NOT WORK
        // take_by_type(InStruct { _x: &x }) // WORKS
        // take_by_trait_output_dyn(InStruct { _x: &x }) // WORKS
    };
}

Is there some elided lifetime here that I could qualify to make this work, or do I need to do heap allocation?

like image 915
Sebastian Holmin Avatar asked Oct 18 '25 13:10

Sebastian Holmin


1 Answers

The impl Trait semantic means that the function returns some type that implements the Trait, but the caller cannot make any assumptions about which type that would be or what lifetimes it would use.

For all the compiler knows the take_by_trait function can be used in many different modules, or, probably, in other crates. Now, your implementation would work fine in all use cases. It can be rewritten like

fn take_by_trait<T: InTrait>(_: T) -> () {}

This is a perfectly fine function and will work just fine. But then at some point you might want to add another implementation for the OutTrait and change your take_by_trait function a little.

trait OutTrait { fn use_me(&self) {} }
impl<T: InTrait> OutTrait for T {}

fn take_by_trait<T: InTrait>(v: T) -> impl OutTrait {v}

If we expand the generic parameter and impl definition, we get this code:

fn take_by_trait<'a>(v: InStruct<'a>) -> InStruct<'a> {v}

fn main() {
    let v = {
        let x = true;
        take_by_trait(InStruct{ _x: &x })
    };
    
    v.use_me();
}

This obviously cannot work because x is dropped before println! tries to access its value. So by adding a new implementation for the OutTrait you broke code that uses this function, potentially somewhere in a crate that depends on yours. That's why the compiler is reluctant to allow you defining such things.

So, again, the issue with impl OutTrait is just that the compiler cannot make any assumptions about the returned type and its lifetime, so it uses the maximum possible bound, which produces the borrow checker error you see.

EDIT: I've modified the code a little so that the signature of the function would not change and the code actually compiles and produces the same lifetime error: playground

like image 66
Maxim Gritsenko Avatar answered Oct 21 '25 06:10

Maxim Gritsenko