conditionally implement trait inside macro invocation

Viewed 191

I'm looking to create a macro to generate thin wrappers for some basic types, for example, the syntax:

wrapper!(String => Email);

would expand to something roughly like:

struct Email {
  inner: String,
}

impl Email {
  // some boilerplate-y stuff
}

However, I'd like these types to implement some stdlib traits where appropriate (e.g. Eq, Clone, Copy, etc. In particular, Copy is causing problems here.

I'd like the wrapper type to be Copy if the inner type is Copy (similar to how #[derive] works), however if I put a derive implementation on the generated trait, I get an error if incompatible:

macro_rules! wrapper {
  ($inner:ty => $name:ident) => {
    #[derive(Copy)] 
    struct $name {
      inner: $inner
    }
  }
}

wrapper!(i64 => Version);  // works fine
wrapper!(String => Email);  // Error, String is not Copy

Instead of a compile-time error when using a non-copy type, I'd rather it just "didn't try to implement Copy", but I'm not sure how to express that in macros.

Another unsuccessful attempt looked like:

// inside macro
impl Copy for $name where $inner: Copy {}

But this gave me the same String is not Copy error. I would have hoped that, during expansion, when it hits: impl Copy for Email where String: Copy it would realise: "String: Copy is not satisfied, so this implementation is not valid" and move on, but instead it just errors.

Is there any way to have a macro expand into different code based on trait implementations?

1 Answers

This is called trivial bounds. This had an RFC, but it was only partially applied. I'm not sure why (neither the RFC nor the tracking issue nor the implementation PR mention a reason).

On nightly, you can do that by turning on the trivial_bounds feature (Playground).

If you are forced to stable, you can use the following ugly trick from this comment:

#[derive(Clone, Copy)]
pub struct Wrapper<'a, T> {
    pub inner: T,
    _marker: std::marker::PhantomData<&'a ()>,
}

macro_rules! wrapper {
    ($inner:ty => $name:ident) => {
        struct $name {
            inner: Wrapper<'static, $inner>,
        }
        impl Copy for $name where for<'a> Wrapper<'a, $inner>: Copy {}
        impl Clone for $name
        where
            for<'a> Wrapper<'a, $inner>: Clone,
        {
            fn clone(&self) -> Self {
                Self {
                    inner: self.inner.clone(),
                }
            }
        }
    };
}

Playground.

The lifetime tricks Rust to think this is a generic struct that it can't check whether it implements Copy.

Related