标签 宏参数传递 下的文章

在上一节中,我们希望将一个宏的展开结果,作为参数传递给另一个宏,但是编译器阻止了我们。在宏编程的道路上从来都没有捷径可以走,在这一点上 Rust 和 C++ 是相同的。
既然 Rust 无法将宏的展开结果作为另外一个宏的参数,那么我们在宏内部调用另外一个宏不就可以了吗?

macro_rules! base_vtable_fields
{
    () => { define_struct!(Base func1 func2); };
}
macro_rules! derive1_vtable_fields
{
    () => { define_struct!(Derive1 func1 func2 func3); };
}

如此一来,问题又回到了原点,派生类不知道基类的有哪些虚方法,也就是说 derive1_vtable_fields 的实现必须要调用base_vtable_fields 才可以。于是,最终的宏被定义成下面的样子:宏的用法有了变化,所以宏名称也要适应变化,变量 $name 用来传递结构体名字,变量 $field 用于派生类扩展结构体成员。

macro_rules! base_define_vtable
{
    ($name: ident $($field: ident)*) =>
    { define_struct!($name func1 func2 $($field)*); };
}
macro_rules! derive1_define_vtable
{
    ($name: ident $($field: ident)*) =>
    { base_define_vtable!($name func3 $($field)*); };
}
macro_rules! derive2_define_vtable
{
    ($name: ident $($field: ident)*) =>
    { derive1_define_vtable!($name $($field)*); };
}
base_define_vtable!(BaseVTable);
derive1_define_vtable!(Derive1VTable);
type Derive2VTable = Derive1VTable;

因为 Derive2 没有定义新的虚函数,所以它和 Derive1 的虚表是一样的,因此 Derive2 的虚表直接重用了 Derive1 的虚表。但 derive2_define_vtable 宏必不可少,因为派生类还需要它。

接下来就要解决虚表的初始化问题。虚表的初始化相对来说,复杂一些,我们要考虑三种情况:
1.virtual 方法;
2.override 方法;
3.基类定义的方法而在派生类中没有重写的方法。
我们可以这样定义宏 init_vtable

macro_rules! init_vtable
{
    ($name:ident $(: $base:ident)?, $($base_vfns:ident)*, $($new_vfns:ident)*, $($over_vfns:ident)*) => {...};
}

其中 $name 为类名,$base 为基类名,是可选的,$base_vfns 为基类的虚函数列表,$new_vfns 为派生类新增的虚函数列表,$over_vfns 为派生类重写的虚函数列表。在正式初始化之前,要做一些基本的检查:
1.如果没有基类,那么基类的虚函数列表也不应该有;
2.如果有基类,那么基类的虚函数表不可以没有;
3.派生类新增的虚函数不可以和基类的虚函数重名,如果有,要求用户改用 override 关键字;
4.派生类重写的虚函数如果在基类的虚函数列表中不存在,要求用户改用 virtual 关键字。
我们还没有处理重写方法的函数签名检查,目前我们还做不到这一点,不过也不用担心,如果函数签名不匹配,编译器会报错。
做完这些事情之后,我们遍历基类的虚函数列表,如果虚函数被重写,则用重写的函数的指针来初始化,否则用基类的虚表来初始化它,然后遍历新增虚函数列表,用实现的函数指针初始化。
看到这里,你们应该也发现了:规则宏做不了这样的事情,要用函数式宏,限于篇幅具体代码就不贴出来了。
接下来就是如何将参数传递给 init_vtable 宏,有了上面实现定义虚表宏的经验,实现初始化操作也就不难了:

macro_rules! base_init_vtable
{
    ($name:ident $(: $base:ident)?, $($vfns:ident)*, $($nvfns:ident)*, $($ofns:ident)*) =>
    { init_vtable!($name $(: $base)?, func1 func2 $($vfns)*, $($nvfns)*, $($ofns)*); };
}
macro_rules! derive1_init_vtable
{
    ($name:ident $(: $base:ident)?, $($vfns:ident)*, $($nvfns:ident)*, $($ofns:ident)*) =>
    { base_init_vtable!($name $(: $base)?, func3 $($vfns)*, $($nvfns)*, $($ofns)*); };
}
macro_rules! derive2_init_vtable
{
    ($name:ident $(: $base:ident)?, $($vfns:ident)*, $($nvfns:ident)*, $($ofns:ident)*) =>
    { derive1_init_vtable!($name $(: $base)?, $($vfns)*, $($nvfns)*, $($ofns)*); };
}
init_vtable!(Base,, func1 func2,);                 // 初始化 BaseVTable
base_init_vtable!(Derive1 : Base,, func3, func1);  // 初始化 Derive1VTable
derive1_init_vtable!(Derive2 : Derive1,,, func2);  // 初始化 Derive2VTable

我们为每个类都生成了相应的 xxx_init_vtable 宏,但初始化类自己的虚表时却要调用基类的初始化宏,换句话说,每个类的初始化宏都是为派生类服务的。
为了将一个宏的展开结果传递给另外一个宏,我们绕的圈子太远了,但我们又不得不绕这样的圈子。但是上面的宏定义也确实过于复杂了,而且很多参数又是原样传递的,得想办法优化一下,我们发现在几个宏定义中,只有 vfns 参数发生变化,我们将不变的参数压缩一下:

macro_rules! base_init_vtable
{
    ($($name:ident):+, $($vfns:ident)*, $($params:tt)*) =>
    { init_vtable!($($name):+, func1 func2 $($vfns)*, $($params)*); };
}
macro_rules! derive1_init_vtable
{
    ($($name:ident):+, $($vfns:ident)*, $($params:tt)*) =>
    { base_init_vtable!($($name):+, func3 $($vfns)*, $($params)*); };
}
macro_rules! derive2_init_vtable
{
    ($($params:tt)*) => { derive1_init_vtable!($($params)*); };
}

我们将头部的 $name:ident $(: $base:ident)? 压缩为 $($name:ident):+ ,这一点容易理解,当然这里的语义也不那么严格了,比如,调用者可以传递 x:y:z 这样的参数,但也不必过于担心,毕竟最终调用的 init_vtable 宏会拒绝这样的参数。
我们将尾部的 $($nvfns:ident), $($ofns:ident) 压缩为 $($params:tt)* ,你可以已经注意到了,我们用了一个新的类型 tt 用于匹配剩余的参数,tt 意为标记树,可以匹配任何宏参数,且不改变语义,因此用它来匹配剩余参数,最合适不过了。
其中 derive2_init_vtable 宏由于所有参数都是原样传递,所有参数都压缩为 $($params:tt)* 一个参数。受此启发,我们还可以更进一步优化,只要我们将 init_vtable 宏的传参顺序更改一下,我们将经常会发生变化的部分提前,作为第一个参数,如下:

macro_rules! init_vtable
{
    ($($base_vfns:ident)*, $name:ident $(: $base:ident)?, $($new_vfns:ident)*, $($over_vfns:ident)*) => {...};
}

那么上面的宏就可以进一步简化为下面的形式,因为参数的顺序改变了,调用方式也有变化:

macro_rules! base_init_vtable
{
    ($($params:tt)*) => { init_vtable!(func1 func2 $($params)*); };
}
macro_rules! derive1_init_vtable
{
    ($($params:tt)*) => { base_init_vtable!(func3 $($params)*); };
}
macro_rules! derive2_init_vtable
{
    ($($params:tt)*) => { derive1_init_vtable!($($params)*); };
}
init_vtable!(,Base, func1 func2,);                 // 初始化 BaseVTable
base_init_vtable!(,Derive1 : Base, func3, func1);  // 初始化 Derive1VTable
derive1_init_vtable!(,Derive2 : Derive1,, func2);  // 初始化 Derive2VTable

我们把 define_struct 宏的参数顺序也该一下:

macro_rules! define_struct
{
    ($($field:ident)*, $name:ident) => { ... };
}

然后 xxx_define_vtable 宏,也可以优化成下面的样子:

macro_rules! base_define_vtable
{
    ($($params:tt)*) => { define_struct!(func1 func2 $($params)*); };
}
macro_rules! derive1_define_vtable
{
    ($($params:tt)*) => { base_define_vtable!(func3 $($params)*); };
}
macro_rules! derive2_define_vtable
{
    ($($params:tt)*) => { derive1_define_vtable!($($params)*); };
}
base_define_vtable!(, BaseVTable);
derive1_define_vtable!(, Derive1VTable);
type Derive2VTable = Derive1VTable;

细心的你可能已经发现 xxx_define_vtable 和 xxx_init_vtable 两组宏传参的过程是相同的,只是最终调用的宏不同,现在我们将这唯一的不同也提取出来,作为回调参数,从而将两组宏合并为一组宏,如下:

macro_rules! base_vtable_option
{
    ($callback:ident $($params:tt)*) =>
    { $callback!(func1 func2 $($params)*); };
}
macro_rules! derive1_vtable_option
{
    ($callback:ident $($params:tt)*) =>
    { base_vtable_option!($callback func3 $($params)*); };
}
macro_rules! derive2_vtable_option
{
    ($callback:ident $($params:tt)*) =>
    { derive1_vtable_option!($callback:ident $($params)*); };
}

宏定义中多了一个回调参数,等下我们再想办法优化下,现在我们可以通过 xxx_vtable_option 系列宏来实现定义虚表和初始化虚表两组操作。

base_vtable_option!(define_struct, BaseVTable);
derive1_vtable_option!(define_struct, Derive1VTable);
derive2_vtable_option!(define_struct, Derive2VTable);

init_vtable!(, Base, func1 func2,);                 // 初始化 BaseVTable
base_vtable_option!(init_vtable, Derive1 : Base, func3, func1);  // 初始化 Derive1VTable
derive1_vtable_option!(init_vtable, Derive2 : Derive1,, func2);  // 初始化 Derive2VTable
derive2_vtable_option!(init_vtable, Derive3 : Derive2, func4, func1);  // 假设 Derive3 存在

定义虚表的操作看起来没什么问题,但是初始化基类虚表和派生类虚表的调用的宏格式不一致。带着这个问题,和多一个参数的问题,我们再进一步对宏定义进行优化。和之前的优化思路是一样的,将可变的部分提前,作为第一个参数,于是回调参数只能作为第二个参数了:

macro_rules! vtable_option
{
    ($($func:ident)*, $callback:ident $($params:tt)*) =>
    { $callback!($($func)* $($params)*); };
}
macro_rules! base_vtable_option
{
    ($($params:tt)*) => { vtable_option!(func1 func2 $($params)*); };
}
macro_rules! derive1_vtable_option
{
    ($($params:tt)*) => { base_vtable_option!(func3 $($params)*); };
}
macro_rules! derive2_vtable_option
{
    ($($params:tt)*) => { derive1_vtable_option!($($params)*); };
}

我们新增了一个宏 vtable_option 来处理参数的顺序,其他的宏只需要按部就班传递参数即可,我们再看一下宏的调用:

base_vtable_option!(,define_struct, BaseVTable);
derive1_vtable_option!(,define_struct, Derive1VTable);
derive2_vtable_option!(,define_struct, Derive2VTable);

vtable_option!(,init_vtable, Base, func1 func2,);                      // 初始化 BaseVTable
base_vtable_option!(,init_vtable, Derive1 : Base, func3, func1);       // 初始化 Derive1VTable
derive1_vtable_option!(,init_vtable, Derive2 : Derive1,, func2);       // 初始化 Derive2VTable
derive2_vtable_option!(,init_vtable, Derive3 : Derive2, func4, func1); // 假设 Derive3 存在

所有虚表的初始化操作格式也都一致了。
虽然 Rust 不支持将一个宏的展开结果直接传递给另一个宏使用,但我们通过回调模式找到了一条极简的路。但同时极简也意味着极复杂,宏的定义简单了,但宏调用代码也越发的难以理解了。
至此,挡在我们目标面前最大的一座山已经翻过去了。接下来我们来实现虚方法和重写方法。