在 Rust 中模拟 C++ 类的功能 宏回调模式第五
在上一节中,我们希望将一个宏的展开结果,作为参数传递给另一个宏,但是编译器阻止了我们。在宏编程的道路上从来都没有捷径可以走,在这一点上 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 不支持将一个宏的展开结果直接传递给另一个宏使用,但我们通过回调模式找到了一条极简的路。但同时极简也意味着极复杂,宏的定义简单了,但宏调用代码也越发的难以理解了。
至此,挡在我们目标面前最大的一座山已经翻过去了。接下来我们来实现虚方法和重写方法。
新盘新项目,不再等待,现在就是最佳上车机会!
新项目准备上线,寻找志同道合的合作伙伴coinsrore.com
2025年10月新盘 做第一批吃螃蟹的人coinsrore.com
新车新盘 嘎嘎稳 嘎嘎靠谱coinsrore.com
新车首发,新的一年,只带想赚米的人coinsrore.com
新盘 上车集合 留下 我要发发 立马进裙coinsrore.com
做了几十年的项目 我总结了最好的一个盘(纯干货)coinsrore.com
新车上路,只带前10个人coinsrore.com
新盘首开 新盘首开 征召客户!!!coinsrore.com
新项目准备上线,寻找志同道合的合作伙伴coinsrore.com
新车即将上线 真正的项目,期待你的参与coinsrore.com
新盘新项目,不再等待,现在就是最佳上车机会!coinsrore.com
新盘新盘 这个月刚上新盘 新车第一个吃螃蟹!coinsrore.com
2025年10月新盘 做第一批吃螃蟹的人coinsrore.com
新车新盘 嘎嘎稳 嘎嘎靠谱coinsrore.com
新车首发,新的一年,只带想赚米的人coinsrore.com
新盘 上车集合 留下 我要发发 立马进裙coinsrore.com
做了几十年的项目 我总结了最好的一个盘(纯干货)coinsrore.com
新车上路,只带前10个人coinsrore.com
新盘首开 新盘首开 征召客户!!!coinsrore.com
新项目准备上线,寻找志同道合 的合作伙伴coinsrore.com
新车即将上线 真正的项目,期待你的参与coinsrore.com
新盘新项目,不再等待,现在就是最佳上车机会!coinsrore.com
新盘新盘 这个月刚上新盘 新车第一个吃螃蟹!coinsrore.com
2025年10月新盘 做第一批吃螃蟹的人coinsrore.com
新车新盘 嘎嘎稳 嘎嘎靠谱coinsrore.com
新车首发,新的一年,只带想赚米的人coinsrore.com
新盘 上车集合 留下 我要发发 立马进裙coinsrore.com
做了几十年的项目 我总结了最好的一个盘(纯干货)coinsrore.com
新车上路,只带前10个人coinsrore.com
新盘首开 新盘首开 征召客户!!!coinsrore.com
新项目准备上线,寻找志同道合 的合作伙伴coinsrore.com
新车即将上线 真正的项目,期待你的参与coinsrore.com
新盘新项目,不再等待,现在就是最佳上车机会!coinsrore.com
新盘新盘 这个月刚上新盘 新车第一个吃螃蟹!coinsrore.com