Rust宏接受带有泛型参数的类型

20

我有一个实现trait的宏,impl_Trait!()。目前,它适用于没有泛型参数的类型,但我不确定如何在impl关键字中添加类型参数。

macro_rules! impl_FooTrait {
    ($name:ty) => {
        impl $crate::FooTrait for $name { ... }
    };
}

struct Bar(i32);
impl_FooTrait!(Bar);
// All OK

struct Baz<'a>(&'a i32);
impl_FooTrait!(Baz<'a>);
// use of undeclared lifetime name `'a`

我不是宏专家(我真的很想深入研究它们),但这里有一个选项你可以尝试。看起来有点奇怪..使用生命周期然后声明它。不过它似乎能够工作。 - Simon Whitehead
3个回答

21
首先,在使用macro_rules!解析泛型的时候,要做到完全无误几乎是不可能的(甚至可能是不可能的),因为模式不支持混合重复(例如$( $( $lt:lifetime ) | $( $gen:ident )* )*,它将匹配生命周期('a)或者一个泛型参数(T))。
如果确实需要这样做,可以考虑使用proc-macro(使用proc-macro-hack可以将它们甚至放在表达式位置)。
简单地在此处放置代码而不加说明对任何人都没有好处,因此以下步骤将详细介绍理解最终声明性宏所需的所有步骤 :)
Hello<'a, 'b>Hello的形式解析输入相对较简单:
macro_rules! simple_match {
    (
        // name of the struct/enum
        $name:ident
        // only one or none `<>`
        $(<
            // match one or more lifetimes separated by a comma
            $( $lt:lifetime ),+
        >)?
    ) => {}
}

simple_match!( Hello<'a, 'b, 'static> );

您可能还有受限生命周期(例如Hello<'a, 'b: 'a, 'static>),这不能用以上方法解析。

为了解析这个,需要在$lt:lifetime的末尾添加以下模式:

// optional constraint: 'a: 'b
$( : $clt:lifetime )?

macro_rules! better_match {
    (
        // name of the struct/enum
        $name:ident
        // only one or none `<>`
        $(<
            // match one or more lifetimes separated by a comma
            $(
                $lt:lifetime
                // optional constraint: 'a: 'b
                $( : $clt:lifetime )?
            ),+
        >)?
    ) => {}
}

better_match!( Hello<'a, 'b: 'static> );

上述限制仅适用于单个受限生命周期(Hello<'a: 'b + 'c>将无法解析)。为了支持多个受限生命周期,必须更改模式为:
$(
    : $clt:lifetime
    // allow `'z: 'a + 'b + 'c`
    $(+ $dlt:lifetime )*
)?

这就是解析通用生命周期所需的全部内容。也可以尝试解析更高级别的生命周期,但这会使模式变得更加复杂。

因此,解析生命周期的最终宏看起来像这样:

macro_rules! lifetimes {
    ( $name:ident $(< $( $lt:lifetime $( : $clt:lifetime $(+ $dlt:lifetime )* )? ),+ >)? ) => {}
}

lifetimes!( Hello<'b, 'a: 'b, 'static, 'c: 'a + 'b> );

上述宏仅允许生命周期,可以通过在模式中将lifetime替换为tt来修复此问题(生命周期和通用参数都可以解析为tt):

macro_rules! generic {
    ( $name:ident $(< $( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+ >)? ) => {}
}

generic!( Hello<'b, 'a: 'b, 'static, 'c: 'a + 'b> );
generic!( Hello<T: Display, D: Debug + 'static + Display, 'c: 'a + 'b> );

如我之前提到的,目前似乎无法区分lifetime和trait bound。如果需要区分,可以部分地使用($(+ $lt:lifetime)* $(+$param:ident)*),但这对于类似于Hello<'a, T, 'b>T: 'a + Debug + 'c这样的未排序边界是不起作用的。


impl_trait宏将会像这样编写:

use std::fmt::{Debug, Display};

trait ExampleTrait {}

struct Alpha;
struct Beta<'b>(&'b usize);
struct Gamma<T>(T);
struct Delta<'b, 'a: 'static + 'b, T: 'a, D: Debug + Display + 'a> {
    hello: &'a T,
    what: &'b D,
}

macro_rules! impl_trait {
    ( $name:ident $(< $( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+ >)? ) => {
        // I split this over multiple lines to make it more readable...
        // this is essentially just a copy of the above match without the
        // type annotations
        impl $(< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)?
            ExampleTrait
        for $name
            // the bounds are not required here
            $(< $( $lt ),+ >)?
        {}
    }
}

impl_trait!(Alpha);
impl_trait!(Beta<'b>);
impl_trait!(Gamma<T>);
impl_trait!(Delta<'b, 'a: 'static + 'b, T: 'a, D: Debug + Display + 'a>);
注意: 不支持路径(例如 impl_trait!(Hello<D: std::fmt::Display>)
下面的宏可与多个结构体一起使用。
macro_rules! impl_trait_all {
    ( $( $name:ident $(< $( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+ >)? ),+ ) => {
        $(
            // I split this over multiple lines to make it more readable...
            // this is essentially just a copy of the above match without the
            // type annotations
            impl $(< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)?
                ExampleTrait
            for $name
                // the bounds are not required here
                $(< $( $lt ),+ >)?
            {}
        )+
    }
}

impl_trait_all!(
    Alpha,
    Beta<'b>,
    Gamma<T>,
    Delta<'b, 'a: 'static + 'b, T: 'a, D: Debug + Display + 'a>
);

Link to playground with all the code


14

您可以使用tt(单个标记)标识符,在另一个宏分支中接受所需的生命周期 (playground链接)。

macro_rules! impl_FooTrait {
    ($name:ty, $lifetime:tt) => {
        impl<$lifetime> $crate::FooTrait for $name {  }
    };
    ($name:ty) => {
        impl $crate::FooTrait for $name {  }
    };
}

struct Bar(i32);
impl_FooTrait!(Bar);

struct Baz<'a>(&'a i32);
impl_FooTrait!(Baz<'a>, 'a); // Use and declare the lifetime during macro invocation

这里有一个实际实现了某些内容的示例

我想这看起来有些奇怪,我很想看到任何其他的答案。可能有更好的方法来做这件事;我还不熟悉宏领域。


3
如果你把$lifetime:tt替换成$($args:tt)*,它将适用于任意数量的泛型参数。例如:impl_FooTrait!(Qux<'a, T>, 'a, T: 'a); - DK.
此外,有一种更加简洁的方法来完成这个任务...但是,坦白地说,如果没有过程宏,这种方法就太荒谬了,在这里不值得深入讨论。如果你好奇的话,可以查看parse-generics-shim crate的源代码,但尽量不要疯掉。 - DK.

4

我有一个部分解决方案,但我无法让它适用于生命周期参数。

#[macro_export]
macro_rules! impl_trait {
    // this evil monstrosity matches <A, B: T, C: S+T>
    ($ty:ident < $( $N:ident $(: $b0:ident $(+$b:ident)* )? ),* >) =>
    {
        impl< $( $N $(: $b0 $(+$b)* )? ),* >
            $crate::path::to::Trait
            for $ty< $( $N ),* >
        {
            // function implementations go here
        }
    };
    // match when no type parameters are present
    ($ty:ident) => {
        impl_trait!($ty<>);
    };
}

示例(Playground)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接