dtolnay / async-trait

Type erasure for async trait methods
Apache License 2.0
1.84k stars 85 forks source link

`#[cfg(..)]` gated impl parameters with lifetimes cause inconsistent lifetime bounds #243

Closed azriel91 closed 1 year ago

azriel91 commented 1 year ago

Heya, I came across the following compilation error:

Scenario

#[async_trait]
pub trait Trait {
    async fn cfg_param(&self, param: &u8);
    //       ---------------------------- lifetimes in impl do not match this method in trait
}

struct Struct;

#[async_trait]
impl Trait for Struct {
    async fn cfg_param(&self, #[cfg(any())] param: &u8, #[cfg(all())] _unused: &u8) {}
    //       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ lifetimes do not match method in trait
}

This is similar to #226, but in this case the parameters have lifetimes.

The expanded code shows what's happening:

pub trait Trait {
    fn cfg_param<'life0, 'life1, 'async_trait>(
        &'life0 self,
        param: &'life1 u8,
    ) -> ::core::pin::Pin<Box<dyn ::core::future::Future<Output = ()> + ::core::marker::Send + 'async_trait>>
    where
        'life0: 'async_trait,
        'life1: 'async_trait,
        Self: 'async_trait;
}

struct Struct;
impl Trait for Struct {
    fn cfg_param<'life0, 'life1, 'life2, 'async_trait>(   // <-- all `'lifeN` parameters are present
        &'life0 self,
        #[cfg(all())]
        _unused: &'life2 u8,
    ) -> ::core::pin::Pin<Box<dyn ::core::future::Future<Output = ()> + ::core::marker::Send + 'async_trait>>
    where
        'life0: 'async_trait,
        'life1: 'async_trait,  // <-- all `'lifeN` parameters are present
        'life2: 'async_trait,
        Self: 'async_trait,
    {
        Box::pin(async move {
            let __self = self;
            let _: () = {};
        })
    }
}

Options

(that I can think of)

  1. It's not possible to have attributes in type parameter position, so we can't just copy the #[cfg] attributes per lifetime.
  2. Dynamically working out which parameters are part of the same lifetime group is not necessarily easy (e.g. if someone has multiple #[cfg(..)] combinations which don't have the same #[cfg(feature_a)], #[cfg(not(feature_a))] pattern).
  3. Adding a custom #[async_trait(same_lifetime_group)] attribute per parameter detracts from the usability.
  4. Generating a different combination of the impl per combination of #[cfg] is possible, but is it worth the complexity?
  5. Not supported by async-trait, but users can create a separate function with the same signature, and in the trait impl they call the separate function.

The last option is:

#[async_trait]
impl Trait for Struct {
    async fn cfg_param(&self, param: &u8) {
        self.cfg_param_inherent(param).await
    }
}

impl Struct {
    async fn cfg_param_inherent(&self, #[cfg(any())] param: &u8, #[cfg(all())] _unused: &u8) {}
}

// or even simpler, consumers *could* do:
impl Trait for Struct {
    async fn cfg_param(&self, param: &u8) {
        #[cfg(all())]
        let _unused = param;
        // ..
    }
}

Do you think async-trait should support this case? It may not be worth supporting, since option 4 is hard to get right, and may be hard to maintain in the long run / have good diagnostics for.

dtolnay commented 1 year ago

I would prefer to go with option 5. I think this is not worth supporting in async-trait.

Thanks for the clear writeup.