Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
azriel91 opened this issue Mar 22, 2023 · 1 comment

Comments

@azriel91
Copy link
Contributor

azriel91 commented Mar 22, 2023

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
Copy link
Owner

dtolnay commented Nov 12, 2023

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

Thanks for the clear writeup.

@dtolnay dtolnay closed this as completed Nov 12, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants