上一节我们尝试将派生类指针赋值给基类指针,并验证对象能否正确析构。结果不尽如人意。
下面我们就来实现自己的智能指针,来保证对象能够被正确析构,所占用空间能够正常释放,如下:

pub struct DynBox<T: TypeInfoTrait>
{
    ptr: std::ptr::NonNull<T>,
    _marker: std::marker::PhantomData<T>,
}
unsafe impl<T> Send for DynBox<T> where T: TypeInfoTrait + Send {}
unsafe impl<T> Sync for DynBox<T> where T: TypeInfoTrait + Sync {}

智能指针名为 DynBox,参数 T 要求实现 TypeInfoTrait,也就是说我们的代码生成的类专用,成员 ptr 顾名思义,指向对象的地址,_marker 用来标记 T 类型对象被持有,以便编译器 drop checker 能正常工作,而代价是,没有代价,PhantomData 不占用空间。
Send 和 Sync 这一对,懂的都懂,不懂的也都不懂,如我。解释不清楚,索性不解释。
下面开始实现 new 方法:

impl<T: TypeInfoTrait> DynBox<T>
{
    pub fn new<U: TypeInfoTrait>(value: U) -> Self
    {
        if !T::get_typeinfo().is_base_of(U::get_typeinfo())
        {
            panic!("can not convert type {} to {}.", std::any::type_name::<U>(), std::any::type_name::<T>());
        }
        let layout = U::get_typeinfo().layout();
        assert_ne!(layout.size(), 0, "not support.");
        let ptr = unsafe { std::alloc::alloc(*layout) };
        if ptr.is_null()
        {
            std::alloc::handle_alloc_error(*layout);
        }
        let ptr_u = ptr as *mut U;
        unsafe { std::ptr::write(ptr_u, value); }
        let ptr = unsafe { std::ptr::NonNull::new_unchecked(ptr as *mut T) };
        Self { ptr, _marker: std::marker::PhantomData }
    }
}

这里通过类型信息来判断类型 &U 是否可以转换为类型 &T,有一定的开销,但这也是目前最简单和有效的实现了。
代码里面的 panic!() 和 std::alloc::handle_alloc_error() 都是发散函数,不会返回。如果走到这里,不用担心代码还会继续向下走。
虽然在 C++ 中实现一个智能指针并不难,但在 Rust 中如何实现智能指针还是难倒了我。为此我参考了标准库 Box 的实现,然而源代码一个 #[rustc_box] 就给我打发了,远不如 ida pro 可靠。
不得不说,Rust 分配内存需要同时指定大小及对齐两个参数,比起 C++ 进步了很多。代价就是这两个参数在释放内存时还要用,为了不增加指针的大小,我将他们放在类型信息里面了:

pub struct TypeInfo
{
    base_class: Option<&'static TypeInfo>,
    layout: std::alloc::Layout,
}
impl TypeInfo
{
    fn layout(&self) -> &std::alloc::Layout { &self.layout }
    ...
}

指针的创建过程完成了,为了智能指针用起来像一个指针,不,是引用,我们还需要实现 Deref 和 DerefMut:

impl<T: TypeInfoTrait> Deref for DynBox<T>
{
    type Target = T;
    fn deref(&self) -> &Self::Target
    {
        unsafe { self.ptr.as_ref() }
    }
}
impl<T: TypeInfoTrait> DerefMut for DynBox<T>
{
    fn deref_mut(&mut self) -> &mut Self::Target
    {
        unsafe { self.ptr.as_mut() }
    }
}

类型转换方法也需要实现:

impl<T: TypeInfoTrait> DynBox<T>
{
    ...
    pub fn dynamic_cast<U: TypeInfoTrait>(&self) -> Option<&U>
    {
        crate::dynamic_cast::<T, U>(unsafe { self.ptr.as_ref() })
    }
    pub fn dynamic_cast_mut<U: TypeInfoTrait>(&mut self) -> Option<&mut U>
    {
        crate::dynamic_cast_mut::<T, U>(unsafe { self.ptr.as_mut() })
    }
}

上面两个方法只是简单的包装了全局的同名方法,主要是为了代码书写方便,举个例子:

let b: DynBox<Base> = DynBox::new::(Derive::new(...));
// 使用全局方法
let d1 = dynamic_cast::<Base, Derive>(&b);
// 使用成员方法
let d2 = b.dynamic_cast::<Derive>();

很明显,使用成员方法的更加简洁。
现在这个智能指针已经可以用了,但这并不是我们的目标。
最后,也是最重要的,是要正确调用析构函数及释放内存。当我们将类型 U 的对象传递给 DynBox 的时候,就已经丢失了 U 的类型信息,因此要正确调用到 U 的析构函数有一定的难度,但这件事在 C++ 里有成熟的解决方案,就是虚析构函数。之前我们已经实现了虚函数,因此实现虚析构函数并不难,如下:

#[repr(C)]
pub struct BaseVTable
{
    _type_info_: &'static TypeInfo,
    drop: fn(this: *mut Base),
    ...
}
#[repr(C)]
pub struct Derive1VTable
{
    _type_info_: &'static TypeInfo,
    drop: fn(this: *mut Derive1),
    ...
}
#[repr(C)]
pub struct Derive2VTable
{
    _type_info_: &'static TypeInfo,
    drop: fn(this: *mut Derive2),
    ...
}

虚表的第一个槽位是类型信息,第二个槽位为虚析构函数,和我们之前定义的其他虚函数不同,虚析构函数的 this 类型就是当前类,而不是固定为某一个基类,这是因为每个类都要实现虚析构函数。有了虚析构函数,我们可以实现 DynBox 的析构函数了:

impl<T: TypeInfoTrait> Drop for DynBox<T>
{
    fn drop(&mut self)
    {
        let ptr = self.ptr.as_ptr();
        unsafe
        {
            let ptr_vtable = ptr as *const *const usize;
            let ptr_drop = (*ptr_vtable).offset(1);
            let ptr_drop = ptr_drop as *const fn(*mut ());
            (*ptr_drop)(ptr as *mut());

            let ptr_typeinfo = ptr as *const *const *const TypeInfo;
            let layout = (&***ptr_typeinfo).layout();
            std::alloc::dealloc(ptr as *mut u8, *layout);
        }
    }
}

我们先找到虚表的第二个槽位,这里是虚析构函数,我们不关心也无法关心函数的实际类型是什么,假设它是 fn(*mut ()),再调用析构函数,最后我们从类型信息中得到布局信息,释放内存。
现在我们都迫不及待的想要实现虚析构函数了,Rust 提供了两个方法可以从指针调用析构函数,分别是 std::ptr::read 方法和 std::ptr::drop_in_place 方法,我们选择 drop_in_place,因为省代码:

fn drop_impl(this: *mut Base) { std::ptr::drop_in_place(this); }
fn drop_impl(this: *mut Derive1) { std::ptr::drop_in_place(this); }
fn drop_impl(this: *mut Derive2) { std::ptr::drop_in_place(this); }

既然如此,我们甚至不必实现这个方法,直接用 std::ptr::drop_in_place:: 即可:

pub const VTABLE: BaseVTable = BaseVTable
{
    _type_info_: &Self::TYPEINFO,
    drop: std::ptr::drop_in_place::<Base>,
    ...
};
pub const VTABLE: Derive1VTable = Derive1VTable
{
    _type_info_: &Self::TYPEINFO,
    drop: std::ptr::drop_in_place::<Derive1>,
    ...
};
pub const VTABLE: Derive2VTable = Derive2VTable
{
    _type_info_: &Self::TYPEINFO,
    drop: std::ptr::drop_in_place::<Derive2>,
    ...
};

现在测试一下虚析构函数到底有没有用:

let mut v = Vec::<DynBox<Base>>::new();
v.push(DynBox::new(Base::new(1, 2)));
v.push(DynBox::new(Derive1::new(1, 2, 3)));
v.push(DynBox::new(Derive2::new(1, 2, 3)));
for b in &v
{
    println!("the result = {:?}.", func(&b));
}

输出如下:

the result = (1, 102, -1).
the result = (3, 102, 3).
the result = (3, 10102, 10003).
Base::drop.
Derive1::drop.
Base::drop.
Derive2::drop.
Derive1::drop.
Base::drop.

没有耽误类的各项功能,而且虚析构函数起作用了。这和我们在堆上创建对象得到的结果是一致的。而且我们现在可以在一个集合中管理某个基类的不同的派生类对象。
下面我们再给测试增加点难度:

pub struct DropTest(i32);
impl Drop for DropTest
{
fn drop(&mut self)
{
    println!("DropTest::drop.");
}
}
#[repr(C)]
pub struct Derive1
{
base: Base,
z: i32,
d: DropTest,
}

我们给 Derive1 增加了一个需要析构的数据成员,当然,Derive2 也会继承这个数据成员,再此运行上面的代码,输出如下:

the result = (1, 102, -1).
the result = (3, 102, 3).
the result = (3, 10102, 10003).
Base::drop.
Derive1::drop.
Base::drop.
DropTest::drop.
Derive2::drop.
Derive1::drop.
Base::drop.
DropTest::drop.

DropTest 的析构函数也自动被调用了。
我们不需要在析构函数中手动调用基类的析构函数,甚至不需要手动调用数据成员的析构函数。就像我们在 C++ 中也不需要做这些事情一样。
实际上,我们的虚析构函数是保障对象能够被正确析构的一种机制,并不是真正的析构函数。类的析构函数还是在 Drop trait 中实现,当然这些细节会隐藏在 class 宏之下,程序员只需要在类体内实现一个 drop(&mut self) 的方法即可。如不需要析构函数,则不需要实现此方法。但虚析构函数机制始终都存在。
至此,我们可以把派生类的对象指针赋值给基类指针,并且可以正常析构和释放内存了。
作为一个功能完备的智能指针,这还不够,下一节我们来完善它。

标签: rust 实现智能指针, rust 虚析构函数, rustc_box, drop_in_place, Rust 模拟 C++

已有 4 条评论

  1. 新盘新盘 这个月刚上新盘 新车第一个吃螃蟹!coinsrore.com

  2. 新车新盘 嘎嘎稳 嘎嘎靠谱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

添加新评论