在 Rust 中模拟 C++ 类的功能 在派生类中调用基类被重写的方法第八
上一节,我们实现了重写函数,但还有些细节还没写完,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") 的代码。
手机出租在线解码+15561262350+http://baidu.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