如何在Rust宏中匹配表达式的类型?

30
这只是伪代码:
macro_rules! attribute {
    $e: expr<f32> => { /* magical float stuff */ };
    $e: expr<i64> => { /* mystical int stuff */ };
};

我希望根据传递给宏的类型,能够有不同的扩展宏。

以下是在C++中的实现方式:

template <typename T>
struct Attribute{ void operator(T)() {} };

template <>
struct Attribute<float> {
    void operator(float)(float) { /* magical float stuff */ }
};

template <>
struct Attribute<long> {
    void operator()(long) { /* mystical int stuff */ }
}

1
宏仅基于语法进行调度,而特质则可以根据类型进行调度。考虑使用特质或在宏内部以某种方式使用特质。 - bluss
3个回答

39

Rust宏无法做到这一点。宏在语法层面上操作,而不是在语义层面上操作。这意味着尽管编译器知道它有一个表达式(语法),但它不知道在扩展宏的时刻该表达式值的类型(语义)是什么。

解决方法是将预期类型传递给宏:

macro_rules! attribute {
    ($e:expr, f32) => { /* magical float stuff */ };
    ($e:expr, i64) => { /* mystical int stuff */ };
}

fn main() {
    attribute!(2 + 2, i64);
}

或者更简单地说,定义多个宏。


如果你想基于表达式的类型进行静态(编译时)分发,可以使用特性。定义一个具有必要方法的 trait,然后为需要的类型实现该 trait。如果 impl 块与 trait 定义在同一个 crate 中,你可以为 任何 类型(包括原始类型和其他库中的类型)实现 trait。

trait Attribute {
    fn process(&self);
}

impl Attribute for f32 {
    fn process(&self) { /* TODO */ }
}

impl Attribute for i64 {
    fn process(&self) { /* TODO */ }
}

macro_rules! attribute {
    ($e:expr) => { Attribute::process(&$e) };
}

fn main() {
    attribute!(2 + 2);
}

注意: 你也可以在宏的主体中写入 $e.process(),但这样可能会调用不相关的process方法。


6
那么在这种情境中,Rust 的宏比 C++ 的模板功能更弱? - Arne
5
如果你希望这样去考虑,那无妨。Rust宏和C++模板只能在大致上进行比较,它们有着不同的功能。然而,两种语言 都能够在这里给你相同的能力,正如trait的例子所证明的那样。我的偏见意见是Rust版本更好,因为你根本不需要模板/宏的元编程——我可能会省略宏,仅在main中调用(2+2).attribute() - Shepmaster
6
@Shepmaster 我不需要静态或动态分派,我需要在编译时得到它的结果。 - Arne
你可以使用 #[inline] 来内联一个函数调用,编译器会优化掉简单的表达式,或者你也可以声明一个 const 函数(现在已经支持)。不过 const 特性还不稳定,这里是相关的跟踪问题:https://github.com/rust-lang/rust/issues/67792 - undefined

10

正如已经解释的那样,您无法根据expr的类型以不同的方式进行扩展。但是作为一种解决方法,您可以使用any模块并尝试从Any特征向下转换:

use std::any::Any;

macro_rules! attribute {
    ( $e:expr ) => {
        if let Some(f) = (&$e as &Any).downcast_ref::<f32>() {
            println!("`{}` is f32.", f);
        } else if let Some(f) = (&$e as &Any).downcast_ref::<f64>() {
            println!("`{}` is f64.", f);
        } else {
            println!("I dunno what is `{:?}` :(", $e);
        }
    };
}

fn main() {
    attribute!(0f32);
    attribute!(0f64);
    attribute!(0);
}

显示:

`0` is f32.
`0` is f64.
I dunno what is `0` :(

11
请注意,这会产生运行时开销,除非编译器能够对其进行优化以消除开销。 - jhpratt
1
我认为这是正确的答案。实际上,我们需要检查编译器是否能够消除任何调用并优化整个表达式。 - Bogdan Mart

3

虽然这里所有的回答都是正确的,但我想提供一个更类似于您C++版本的答案。Rust提供了自己的模板、泛型版本,并且可以像使用模板一样使用它们。因此,要定义一个结构体并为某些类型实现函数:

struct Attribute<T> {
    value: T,
}

impl Attribute<u32> {
    fn call(&self) {
        println!("{} is a u32", self.value);
    }
}

impl Attribute<f64> {
    fn call(&self) {
        println!("{} is a f64", self.value);
    }
}

impl Attribute<String> {
    fn call(&self) {
        println!("{} is a string", self.value);
    }
}

我们会像这样使用它:
fn main() {
    let a = Attribute{
        value: 5_u32
    };


    a.call();
}

或者简单地像这样:
Attribute{value: 6.5}.call()

遗憾的是,Rust 的稳定版本中没有提供 () 操作符重载功能。你仍然可以定义一个宏来完成该任务:

macro_rules! attribute {
    ( $e:expr ) => {
        Attribute{value: $e}.call(); 
    };
}

然后这样使用:

attribute!("Hello World!".to_string());

我建议使用此答案中展示的trait-based方法,因为它不使用struct而是使用trait,这被认为是更好的做法。在许多情况下,此答案仍然可能有帮助。


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