在 Rust 中模拟 C++ 类的功能 智能指针及虚析构函数第十一
上一节我们尝试将派生类指针赋值给基类指针,并验证对象能否正确析构。结果不尽如人意。
下面我们就来实现自己的智能指针,来保证对象能够被正确析构,所占用空间能够正常释放,如下:
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
不得不说,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
#[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) 的方法即可。如不需要析构函数,则不需要实现此方法。但虚析构函数机制始终都存在。
至此,我们可以把派生类的对象指针赋值给基类指针,并且可以正常析构和释放内存了。
作为一个功能完备的智能指针,这还不够,下一节我们来完善它。