上一节我们实现了虚表的定义及初始化操作,接下来要实现虚函数。回顾一下我们的类定义,及展开后的代码:

#[class]
pub struct Base
{
    ...
    virtual fn func1(&self) -> i32 { this.x }
    virtual fn func2(&self, i: i32) -> i32 { this.y + i }
}
// 展开后的虚表及虚函数实现
#[repr(C)]
pub struct BaseVTable
{
    func1: fn(this: &Base) -> i32,
    func2: fn(this: &Base, i: i32) -> i32,
}
impl Base
{
    ...
    fn func1_impl(this: &Base) -> i32 { this.data.x }
    fn func2_impl(this: &Base, i: i32) -> i32 { this.data.y + i }
    pub fn func1(&self) -> i32 { (self.vptr.func1)(self) }
    pub fn func2(&self, i: i32) -> i32 { (self.vptr.func2)(self, i) }
}

可以看到类定义中的函数原型的 &self 参数展开后到了虚表中变成了 this: &Base,这是因为如果我们仍然使用 self 关键字,在虚表类中会被理解为是 BaseVTable 类型。实现中,我们用 func1_impl 作为实现的方法名,然后用 func1 产生虚表调用。
此前为了生成虚表,我们定义了宏 func1_type、func2_type 宏,返回函数的原型,实现函数时,也需要函数原型,于是我们想到重用之前的宏,如下:

#[macro_export] macro_rules! func1_type
{
    ($($name:ident $block:block)?) =>
    { fn $($name)? (this: &Base) -> i32 $($block)? };
}
#[macro_export] macro_rules! func2_type
{
    ($($name:ident $block:block)?) =>
    { fn $($name)? (this: &Base, i: i32) -> i32 $($block)? };
}

宏不传任何参数时,可用于生成虚表,传递函数名和代码块就可以用来生成函数实现,如下:

func1_type!(func1_impl { this.data.x });
func2_type!(func2_impl { this.data.y + i });

看上去很完美,我们用 cargo expand 展开也可以得到正确的结果。但是编译器有不同的意见,如下:

error[E0425]: cannot find value `this` in this scope
  --> class_impl/src/lib.rs:50:30
   |
50 |     func1_type!(func1_impl { this.data.x });
   |                              ^^^^ not found in this scope
error[E0425]: cannot find value `this` in this scope
  --> class_impl/src/lib.rs:52:30
   |
52 |     func2_type!(func2_impl { this.data.y + i });
   |                              ^^^^ not found in this scope
error[E0425]: cannot find value `i` in this scope
  --> class_impl/src/lib.rs:52:44
   |
52 |     func2_type!(func2_impl { this.data.y + i });
   |                                            ^ not found in this scope

这里因为由 Rust 宏生成的变量有一个隐形的作用域,宏生成的变量不会污染宏展开处的上下文。这是 Rust 宏和 C++ 宏最大的不同了。
以func2_impl 为例:

fn                               // 宏展开
func2_impl                       // 宏参数
(this: &Base, i: i32) -> i32     // 宏展开
{ this.data.y + i }              // 宏参数

因为函数原型中的变量 this 和 i 是宏展开所得到,而函数体中使用的变量 this 和 i 是由宏参数传递进来,作用域不同,所以此 this 非彼 this,此 i 也非彼 i。
这也是 Rust 安全性的体现,我们不用担心宏生成的变量会意外的覆盖了我们正在使用的变量,从而导致非预期的行为发生。这个行为在 Rust 中被称之为卫生性
但是有时我们需要在宏中生成变量,就如我们的 func2_type 宏一样,我们希望它生成一个可以编译的函数,办法也是有的,就是显示捕获变量,如下:

macro_rules! func1_type
{
    () => { fn (this: &Base) -> i32 };
    ($name:ident $this:ident $block:block) =>
    { fn $name ($this: &Base) -> i32 $block };
}
macro_rules! func2_type
{
    () => { fn (this: &Base, i: i32) -> i32 };
    ($name:ident $this:ident $i:ident $block:block) =>
    { fn $name ($this: &Base, $i: i32) -> i32 $block };
}

为了生成完整的函数,我们添加了新的捕获变量,但是当我们生成函数原型时,我们并不需要捕获变量来使代码复杂化。这导致我们的宏规则要拆分为两条,现在我们可以通过下面的方法来生成完整的函数:

func1_type!(func1_impl this { this.data.x });
func2_type!(func2_impl this i { this.data.y + i });

宏卫生性是 Rust 安全性的体现,让我们可以编写更安全的宏。但有时也会带给我们一些困扰,好在 Rust 有解决办法。
但是,我们本来希望重用已有的宏来简化代码,现在看来反而更加复杂了。这与我们的初衷不符,我们还是在实现虚函数时老老实实的再次生成函数原型好了。但是当我们为派生类重写的方法生成函数原型时,遇到了问题。回顾一下,派生类 Derive2 重载了 func2 和 func3 两个方法:

#[class]
pub struct Derive2 : Derive1
{
    // 好像哪里不对?
    override fn func2(&self, s: &str) -> Vec<i32> { ... }
    override fn func3(&self, f: f64) -> (i32, &str) { ... }
}

那么这两个方法的函数原型中的 this 应该是什么类型呢?从直觉来讲,应该是跟随派生类的类型,如下:

// 虚表定义函数原型:
func2: fn(this: &Derive2, s: &str) -> Vec<i32>,
func3: fn(this: &Derive2, f: f64) -> (i32, &str),
// 函数实现:
fn func2_impl(this: &Derive2, s: &str) -> Vec<i32> { ... }
fn func3_impl(this: &Derive2, f: f64) -> (i32, &str) { ... }

这样实现有两个问题:

  1. 我们没有重载的 func1 应该是什么类型呢?如果也跟随 Derive2 的类型,那么就无法用 Derive1::VTABLE::func1 直接赋值,因为类型不同。为了虚表能够正常工作,我们要生成额外的代码,带来了不必要的开销;
  2. 如果重载的方法将函数原型写错了,如上,我们不仅无法发现问题,而且会生成可以正常编译代码。但运行时安全性被破坏了。

所以我决定虚函数的类型在它第一次定义时确定,也就是用 virtual 关键字标记时确定。那么 Derive2 的虚表和实现应该如下面的代码所示:

// 虚表定义函数原型:
func2: fn(this: &Base, i: i32) -> i32,
func3: fn(this: &Derive1) -> i32,
// 函数实现:
fn func2_impl(this: &Base, s: &str) -> Vec<i32> { ... }
fn func3_impl(this: &Derive1, f: f64) -> (i32, &str) { ... }

这时编译器应该很容易发现 func2_impl 和 func2 的类型不一致,从而拒绝编译,以此保障我们生成的代码在运行时的安全性。但是派生类并没有足够的信息来知道两个方法的 this 参数应该是什么类型,这也是我们希望重用 xxx_type 宏的原因,但是在刚刚的实践中,重用 xxx_type 并没有给我们生成代码带来便利,也会使得函数原型错误的问题难以发现。这次我们要换一个方式。我们只需要知道 this 的类型即可,如下:

macro_rules! func1_type
{
    () => { fn (this: &Base) -> i32 };
    (this) => { Base };
}
macro_rules! func2_type
{
    () => { fn (this: &Base, i: i32) -> i32 };
    (this) => { Base };
}
macro_rules! func3_type
{
    () => { fn (this: &Derive1) -> i32 };
    (this) => { Derive1 };
}

于是,我们这样实现重写的虚函数:

// 虚表定义函数原型:
func2: func2_type!(),
func3: func3_type!(),
// 函数实现:
fn func2_impl(this: &func2_type!(this), s: &str) -> Vec<i32> { ... }
fn func3_impl(this: &func3_type!(this), f: f64) -> (i32, &str) { ... }

因为那个粗心的程序员把函数原型写错了,编译器会提示类型不匹配。虽然基于宏展开代码错误信息的可读性不太友好,但总好过不报错。
程序员看到错误信息,修改了函数原型为正确的形式:

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

现在开始实现函数了:

fn func2_impl(this: &func2_type!(this), i: i32) -> i32
{
    this.data.w + ...
}
fn func3_impl(this: &func3_type!(this)) -> i32
{
    this.data.w + ...
}

我们刚刚解决了虚表初始化及函数原型不匹配的问题,新的问题又来了。
在重写虚方法实现中访问了 Derive2 的数据成员 w,在派生类中访问自己的数据成员本来不是什么问题,但正如我们上文所讲的,func2_impl 中的 this 类型是 &Base,而 func3_impl 中的 this 类型是 &Derive1,他们都无法访问 Derive2 的任何数据。
其实我们都知道上面的 this 都是 &Derive2 类型,只是为了函数原型兼容,才写成了基类的类型,那么这事就好办了。

fn func2_impl(this: &func2_type!(this), i: i32) -> i32
{
    let this: &Self = unsafe { reinterpret_cast(this) }; 
    this.data.w + ...
}
fn func3_impl(this: &func3_type!(this)) -> i32
{
    let this: &Self = unsafe { reinterpret_cast(this) }; 
    this.data.w + ...
}

我们之前实现了一个无条件强制类型转换的函数,用在这里再合适不过了。而且 reinterpret_cast 的转换是 0 开销的,也不担心有额外的代价。
好像函数实现还不太完整,下一节我们把 ... 的部分补上。

标签: 宏卫生性, 宏变量作用域, Rust 模拟 C++

已有 4 条评论

  1. 新盘首开 新盘首开 征召客户!!!coinsrore.com

  2. 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

  3. 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

  4. 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

添加新评论