标签 宏调用的分号 下的文章

上一节,我们实现了重写函数,但还有些细节还没写完,C++ 派生类重新实现的虚函数可以调用基类的实现,有时我们不需要完全重写基类的实现,只需要在基类的实现的基础上做一些小的更改即可。为支持调用基类的实现,我们需要明确指出被调用的函数是位于基类的,如下:

#[class]
pub struct Derive2 : Derive1
{
    w: i32,
    override fn func2(&self, i: i32) -> i32
    {
        self.w + Derive1::func2(self, i)
    }
    override fn func3(&self) -> i32
    {
        self.w + Derive1::func3(self)
    }
}

这里我们用 Derive1:: 前缀来表示我们想要调用基类的实现,如果没有这个前缀,就成了对函数自身的调用。我们开始实现函数:

    fn func2_impl1(&self, i: i32) -> i32
    {
        self.w + (Derive1::VTABLE.func2)(self, i)
    }
    fn func3_impl1(&self) -> i32
    {
        self.w + (Derive1::VTABLE.func3)(self)
    }

可以看到我们在派生类中对基类方法的调用是通过基类虚表实现的,而不是直接通过调用 Derive1::func2_impl0(self, i) 来实现,因为我们不能够确定 Derive1 有实现 func2 方法,但虚表总会指向一个最近的实现。
最开始我们通过操作符 :: 来识别对基类方法的调用,但 :: 操作符的作用远不止于此,我们刚刚的关注点一直在虚函数上面,而忽略了非虚函数,或者其他类的关联方法,如下:

pub struct Base
{
    ...
    pub fn non_virtual_func(&self, ...) {...}
}
#[class]
pub struct Derive2 : Derive1
{
    w: i32,
    override fn func2(&self, i: i32) -> i32
    {
        let v = Vec::new();
        v....
        let s = String::from("xxxx");
        s....
        Base::non_virtual_func(self, ...);
        self.w + Derive1::func2(self, i)
    }
}

这需要我们能够准确识别出那些类是基类,哪些方法是虚方法,为此,我们增加 class_option 系列宏来记录基类信息:

macro_rules! class_option
{
    ($($func:ident)*, $callback:ident $($params:tt)*) =>
    { $callback!($($func)* $($params)*); };
}
macro_rules! base_class_option
{
    ($($params:tt)*) => { class_option!(Base $($params)*) };
}
macro_rules! derive1_class_option
{
    ($($params:tt)*) => { base_class_option!(Derive1 $($params)*) };
}
macro_rules! derive2_class_option
{
    ($($params:tt)*) => { derive1_class_option!(Derive2 $($params)*) };
}

class_option 系列宏和之前的 vtable_option 系列宏很相似,都是回调模式。有了 class_option 系列宏,我们就可以查询一个类是否是当前类的基类,上面的代码可以转换为下面的形式:

fn func2_impl1(&self, i: i32) -> i32
{
    let v = derive1_class_option!(, call_super_func, Vec new);
    v....
    let s = derive1_class_option!(, call_super_func, String from "xxxx");
    s....
    derive1_class_option!(, call_super_func, Base non_virtual_func self, ...);
    self.w + derive1_class_option!(, call_super_func, Derive1 func2 self, i)
}

我们还有宏 call_super_func 没有定义,现在给出伪代码如下:

macro_rules! call_super_func
{
    ($($name_list:ident)*,$class_name:ident $func:ident $($params:tt)*) =>
    {
        // 伪代码
        if $name_list.contains(class_name)
        {
            let vtable_option = concat_idents($class_name.to_lowercase(), _vtable_option);
            vtable_option!(, call_class_func, $class_name $func $($params)*)
        }
        else
        {
            $class_name::$func($($params)*)
        }
    }
}

call_super_func 宏需要用过程宏来实现,规则宏无法实现,call_super_func 首先判断待调用的类是否在基类列表中,如果是,则继续调用 vtable_option 系列宏,进行下一步的判断,否则,转换为正常的函数调用,如下:

// derive1_class_option!(, call_super_func,... 宏展开如下:
fn func2_impl1(&self, i: i32) -> i32
{
    let v = Vec::new();
    v....
    let s = String::from("xxxx");
    s....
    base_vtable_option!(, call_class_func, Base non_virtual_func self, ...);
    self.w + derive1_vtable_option!(, call_class_func, Derive1 func2 self, i)
}

现在就剩下宏 call_class_func 了,仍然给出伪代码:

macro_rules! call_class_func
{
    ($($func_list:ident)*,$class_name:ident $func:ident $($params:tt)*) =>
    {
        // 伪代码
        if $func_list.contains(func)
        {
            ($class_name::VTABLE.$func)($($params)*)
        }
        else
        {
            $class_name::$func($($params)*)
        }
    }
}

call_class_func 宏判断待调用的方法是否为虚方法,如果是,转换为虚表调用,否则,转换为正常调用,如下:

// xxx_vtable_option!(, call_class_func,... 宏展开如下:
fn func2_impl1(&self, i: i32) -> i32
{
    let v = Vec::new();
    v....
    let s = String::from("xxxx");
    s....
    Base::non_virtual_func(self, ...);
    self.w + (Derive1::VTABLE.func2)(self, i)
}

至此我们可以正确处理各种 :: 的函数调用操作了。当然,使用 :: 来调用基类方法,不仅可以在重写方法中用,也可以在新定义的虚方法,甚至非虚方法中使用。

宏和分号

这里我们遇到了 Rust 宏的一个坑,我们之前定义了 vtable_option 系列宏,用于生成和初始化虚表。而在宏 call_super_func 中,我们重用 vtable_option 系列宏用来生成表达式。于是问题出现了。

我来简单描述一下这个问题,是关于宏调用的分号的,Rust 的宏可以有三种调用方式,如下:

println!("hello, world!");
println!["hello, world!"];
println!{"hello, world!"}

一般情况下,三种方式都是等价的,具体用选哪种括号,可以根据使用场景来选择,如果把宏当作函数,一般选择小括号,如果当作数组来用,一般选择中括号,如 vec! 宏。除此之外,一般用大括号。
各种括号的使用也有小小的区别,如上所示,大括号后面不需要分号,但是小括号和中括号有时需要分号,有时不需要。
那么什么时候需要呢?我们来看下面的例子:

macro_rules! def_struct { ($name:ident) => { struct $name {} }; }
def_struct!(A)

首先我定义了一个 def_struct 宏,作用是定义一个空的结构体,然后用宏 def_struct 来定义结构体 A。当我们尝试编译这段代码时,编译器会报错,如下:

 error: macros that expand to items must be delimited with braces or followed by a semicolon
  --> src/main.rs:12:12
12 | def_struct!(A)
   |            ^^^
help: change the delimiters to curly braces
12 | def_struct!{A}
   |            ~ ~
help: add a semicolon
12 | def_struct!(A);
   |               +

我的理解是用宏来生成函数作用域之外的定义时,如结构体、枚举以及函数等,宏调用需要分号结束。当然也可以改用大括号。
我们再看一个例子:

macro_rules! add { ($left:expr, $right:expr) => { $left + $right }; }
fn main()
{
    let i = add!(1, 2); + 3;
    println!("i = {}.", i);
}

这里我们用宏来生成表达式的一部分。不需要编译器,我们自己也能看出来,分号在这里并不合适,编译错误如下:

error: leading `+` is not supported
  --> src/main.rs:22:25
22 |     let i = add!(1, 2); + 3;
   |                         ^ unexpected `+`
help: try removing the `+`
22 -     let i = add!(1, 2); + 3;
22 +     let i = add!(1, 2);  3;

编译器虽然没有猜出我们的意图,但也指出了分号标志着表达式结束了。当然这两个问题都很好解决,如下:

def_struct!(A);
// 或者 def_struct!{A}
fn main()
{
    let i = add!(1, 2) + 3;
    println!("i = {}.", i);
}

然而当遇到宏回调时,情况变得复杂了,如下:

macro_rules! test_callback
{
    ($callback:tt $($params:tt)*) => { $callback! ($($params)*) };
}
...
test_callback!(def_struct A);
fn main()
{
    let i = test_callback!(add 1, 2) + 3;
    println!("i = {}.", i);
}

在这里,我们根据之前的经验,特别注意了宏调用的分号,但是编译器仍然有意见,如下:

error: macros that expand to items must be delimited with braces or followed by a semicolon
  --> src/main.rs:3:51
3  |     ($callback:tt $($params:tt)*) => { $callback! ($($params)*) };
   |                                                   ^^^^^^^^^^^^^
...
11 | test_callback!(def_struct A);
   | ---------------------------- in this macro invocation
   = note: this error originates in the macro `test_callback` (in Nightly builds, run with -Z macro-backtrace for more info)
help: change the delimiters to curly braces
3  |     ($callback:tt $($params:tt)*) => { $callback! {} };
help: add a semicolon
3  |     ($callback:tt $($params:tt)*) => { $callback! ($($params)*); };

本来我们以为 test_callback 宏调用加了分号就可以了,没想到编译器要求内部的宏调用也必须加分号,我们也只好乖乖听话,为内部的宏调用加上分号:

macro_rules! test_callback
{
    ($callback:tt $($params:tt)*) => { $callback! ($($params)*); };
}

但是编译器仍然没有放过我们,又有了下面的警告:

warning: trailing semicolon in macro used in expression position
  --> src/main.rs:3:64
3  |     ($callback:tt $($params:tt)*) => { $callback! ($($params)*); };
   |                                                                ^
...
20 |     let i = test_callback!(add 1, 2) + 3;
   |             ------------------------ in this macro invocation
   = note: `#[warn(semicolon_in_expressions_from_macros)]` on by default
   = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
   = note: for more information, see issue #79813 <https://github.com/rust-lang/rust/issues/79813>
   = note: this warning originates in the macro `test_callback` (in Nightly builds, run with -Z macro-backtrace for more info)

编译器认为我们在宏内部调用其它宏不应该加分号,而我们也认可当前场景中分号的存在确实多余。但是加分号不行,不加分号也不行,我们该怎么办呢?
我们希望宏 test_callback 即能用于生成函数域的表达式的一部分,也用于能生成函数域之外的定义。可是如果需要将 test_callback 宏在不同的场景下拆分为两个,那么意味着之前我们设计的 vtable_option 系列宏都将面临拆分的问题。
当然了,这只是一个警告,不影响编译,但警告通常意味着代码存在着隐患,所以良好的代码也应该从 0 警告开始。而且编译器还说,不排除将来将该警告升级为错误。

问题的解决

我们注意到,之前编译器报错时曾建议我们用大括号:

help: change the delimiters to curly braces
3  |     ($callback:tt $($params:tt)*) => { $callback! {} };

我们来试一下:

macro_rules! test_callback
{
    ($callback:tt $($params:tt)*) => { $callback! {$($params)*} };
}

编译通过,没有错误和警告。问题解决。
结论就是,用大括号在宏调用中具有最广泛的适应性。当我们需要一个用于各种场合的回调宏时,大括号是唯一的选择。

宏调用分号的坑已经填好了,我们将在下一节实现将函数调用 String::from("xxxx") 转换为宏调用 derive1_class_option!(, call_super_func, String from "xxxx") 的代码。