在 Rust 中模拟 C++ 类的功能 深度复制方法第十二
在 Rust 中,自定义类型要么是复制语义,要么是移动语义,而我更喜欢移动语义。然而移动语义的类型也可能需要复制的操作,这一点 Rust 早就给我们想好了。这就是 Clone Trait,深度复制,实现 Clone Trait 很简单,大部分情况只需要给类型定义添加 #[derive(Clone)] 属性即可:
#[class]
#[derive(Clone)]
pub struct Base
{
x: i32,
y: i32,
...
}不过呢,有时我们希望自行实现 Clone Trait,我们可以这样做:
#[class]
pub struct Base
{
x: i32,
y: i32,
fn clone(&self) -> Self { Self { x: self.x, y: self.y } }
fn clone_from(&mut self, other: &Self) { self.x = other.x; self.y = other.y; }
...
}class 宏识别到类有方法名为 clone 时,会为类实现 Clone Trait,出于优化的目的,你也可以选择同时实现 clone_from 方法。之所以要绕这么大的圈子,还是因为虚表指针的赋值操作不能由用户来操作,必须交给 class 宏来进行,也就是说 class 宏会分析 clone 及 clone_from 方法,添加虚表指针的赋值操作。有了之前在构造函数中初始化虚表指针的经验,为 clone 方法复制虚表指针也不难:
pub struct CloneVPTR<'a>
{
class_name: &'a str,
blocked: bool,
}
impl<'a> VisitMut for CloneVPTR<'a>
{
fn visit_expr_mut(&mut self, expr: &mut Expr)
{
if !self.blocked
{
if let Expr::Struct(expr_stru) = expr
{
if let Some(seg) = expr_stru.path.segments.last()
{
let name = seg.ident.to_string()
if self.class_name == name || "Self" == name
{
expr_stru.fields.push(parse_quote!(vptr: self.vptr));
}
}
}
}
syn::visit_mut::visit_expr_mut(self, expr);
self.blocked = false;
}
}clone 方法不需要处理派生类,因为在基类中就已经完成了对虚表指针的赋值,比构造函数要简单的多。
我们来验证一下刚刚实现的深度复制方法。
fn get_data(base: &Base) -> (i32, i32, i32)
{
let z = if let Some(d1) = dynamic_cast::<Base, Derive1>(base)
{ d1.z } else { -1 };
(base.x, base.y, z)
}
fn print_address_and_data(b: &Base)
{
println!("the address = {:p}, vptr = {:p}, data = {:?}.", b, b.vptr, get_data(b));
}
macro_rules! clone_and_add
{
($expr:expr, $x:expr, $y:expr) => { { let mut c = $expr.clone(); c.x += $x; c.y += $y; c } };
}函数 get_data 用来获得对象的数据,函数 print_address_and_data 用来打印对象的地址和数据,而宏 clone_and_add 用来创建副本并修改数据,以便和原始对象加以区别。有了辅助函数和宏,我们可以写测试方法了:
fn test_clone()
{
let b = CBase::new(1, 2);
let bc = clone_and_add!(b, 100, 100);
let d1 = CDerive1::new(3, 4, 5);
let d1c = clone_and_add!(d1, 200, 200);
let d2 = CDerive2::new(6, 7, 8);
let d2c = clone_and_add!(d2, 300, 300);
print_address_and_data(&b);
print_address_and_data(&bc);
print_address_and_data(&d1);
print_address_and_data(&d1c);
print_address_and_data(&d2);
print_address_and_data(&d2c);
}运行结果如下:
the address = 0x7f1b9727a4d0, vptr = 0x55a1c3339b58, data = (1, 2, -1).
the address = 0x7f1b9727a4e0, vptr = 0x55a1c3339b58, data = (101, 102, -1).
the address = 0x7f1b9727a508, vptr = 0x55a1c3339b98, data = (3, 4, 5).
the address = 0x7f1b9727a520, vptr = 0x55a1c3339b98, data = (203, 204, 5).
the address = 0x7f1b9727a558, vptr = 0x55a1c3339be0, data = (6, 7, 8).
the address = 0x7f1b9727a570, vptr = 0x55a1c3339be0, data = (306, 307, 8).
Derive2::drop.
Derive1::drop.
Base::drop.
Derive2::drop.
Derive1::drop.
Base::drop.
Derive1::drop.
Base::drop.
Derive1::drop.
Base::drop.
Base::drop.
Base::drop.我们新建了三个对象,深度复制了三个对象,共六个对象,地址不同,且对象大小正确,虚表指针正确,数据正确,析构函数的调用顺序和对象创建的顺序相反。符合预期,测试成功。
智能指针的深度复制方法的实现
接下来要为智能指针支持深度复制操作,我想你们已经猜到了,要走虚机制,如虚析构函数一样。但这样一来虚表中又多了一个用户看不见的方法,而且如果以后还需要实现什么虚调用的方法,又要在虚表中增加方法,这不利于虚表的稳定。因此,我将虚 drop 和 虚 clone 方法都移到了类型信息中,不再占用虚表的槽位,如下:
pub struct TypeInfo
{
base_class: Option<&'static TypeInfo>,
layout: std::alloc::Layout,
drop: unsafe fn(*mut ()),
clone: Option<unsafe fn(*const ()) -> *mut ()>,
clone_from: Option<unsafe fn(*mut (), *const ())>,
}如果以后要增加虚调用,直接加在类型信息里,不会破坏二进制的兼容。其中 drop、clone 和 clone_from 方法都抹去了具体类型,而以空元组指针代替,相当于 C++ 的 void*,这是因为智能指针调用这些方法时,不关心也不知道具体的类型,只需要知道参数是一个指针就够了。当然你也可以选择将 TypeInfo 类型定义为模板,这样每个类的 TypeInfo 都是不同的类型,但是如何定义 base_class 的类型会比较有点麻烦。
深度复制和析构函数有两点不同:
- 无论类型是否实现 Drop Trait,都可以被析构,而如果类型不实现 Clone Trait,就没有 clone 和 clone_from 方法可供调用,因此 drop 是必选项,而 clone 和 clone_from 是可选项;
- Rust 内置了 drop_in_place 方法可以供 drop 方法使用,而 clone 和 clone_from 方法要我们自己实现。
我们知道智能指针的数据是在栈上,因此深度复制后的数据也应该是在栈上,这里涉及到内存分配,内存分配我们不陌生,之前为智能指针实现构造函数时的代码正好拿出来用,如下:
unsafe fn alloc_value<T: TypeInfoTrait>(value: T) -> *mut T
{
let layout = T::get_typeinfo().layout();
assert_ne!(std::mem::size_of::<T>(), 0, "not support.");
let ptr = unsafe { std::alloc::alloc(*layout) };
if ptr.is_null()
{
std::alloc::handle_alloc_error(*layout);
}
let ptr = ptr as *mut T;
unsafe { std::ptr::write(ptr, value); }
ptr
}有了函数 alloc_value,我们可以支持栈到栈的深度复制:
unsafe fn clone_ptr<T: TypeInfoTrait + Clone>(value: *const T) -> *mut T
{
alloc_value((*value).clone())
}方法 clone_from 是在现有的对象上直接构造,不需要重新分配内存:
unsafe fn clone_from_ptr<T: TypeInfoTrait + Clone>(value: *mut T, source: *const T)
{
(*value).clone_from(&*source)
}无论是 alloc_value 还是 clone_ptr 都涉及到内存分配和解引用原生指针,为防止误用,我标记他们为不安全的。
支持深度复制的类,类型信息的 clone 方法指针,都会指向 clone_ptr 方法。我相信你们也看到了,clone_ptr 方法和 TypeInfo 类型的 clone 成员类型不一致,解决方法是强制类型转换, clone_from 方法同理:
const unsafe fn convert_clone_fn<T>(f: unsafe fn(*const T) -> *mut T)
-> unsafe fn (*const ()) -> *mut ()
where T: TypeInfoTrait + Clone,
{
let p = &f as *const unsafe fn(*const T) -> *mut T;
let p = p as *const unsafe fn (*const()) -> *mut();
*p
}
const unsafe fn convert_clone_from_fn<T>(f: unsafe fn(*mut T, *const T))
-> unsafe fn (*mut (), *const ())
{ ... }
pub const fn new<T>(base_class: Option<&'static TypeInfo>) -> Self
where T: TypeInfoTrait + Clone,
{
let layout = std::alloc::Layout::new::<T>();
let drop = unsafe { Self::convert_drop_fn(std::ptr::drop_in_place::<T>) };
let clone = unsafe { Self::convert_clone_fn(clone_ptr::<T>) };
let clone_from = unsafe { Self::convert_clone_from_fn(clone_from_ptr::<T>) };
Self { base_class, layout, drop, clone: Some(clone), clone_from: Some(clone_from) }
}下面为智能指针实现深度复制方法,如下:
impl<T: TypeInfoTrait + Clone> Clone for DynBox<T>
{
fn clone(&self) -> Self
{
let ptr = self.ptr.as_ptr();
unsafe
{
let ptr_typeinfo = ptr as *const *const *const TypeInfo;
let new_ptr = ((&***ptr_typeinfo).clone.unwrap())(ptr as *const());
let ptr = std::ptr::NonNull::new_unchecked(new_ptr as *mut T);
Self { ptr, _marker: std::marker::PhantomData }
}
}
}clone 方法的实现很简单,找到类型信息,然后调用 clone 方法,得到新的内存,构造新的智能指针,工作完成。
clone_from 方法的实现稍显复杂:
fn clone_from(&mut self, source: &Self)
{
let mut self_ptr = self.ptr.as_ptr();
let source_ptr = source.ptr.as_ptr();
unsafe
{
let self_typeinfo = &***(self_ptr as *const *const *const TypeInfo);
let source_typeinfo = &***(source_ptr as *const *const *const TypeInfo);
(self_typeinfo.drop)(self_ptr as *mut());
if self_typeinfo.layout() != source_typeinfo.layout()
{
std::alloc::dealloc(self_ptr as *mut u8, *self_typeinfo.layout());
let ptr = std::alloc::alloc(*source_typeinfo.layout());
if ptr.is_null()
{
std::alloc::handle_alloc_error(*source_typeinfo.layout());
}
self_ptr = ptr as *mut T;
}
(source_typeinfo.clone_from.unwrap())(self_ptr as *mut(), source_ptr as *const());
self.ptr = std::ptr::NonNull::new_unchecked(self_ptr as *mut T);
}
}我们首先拿到两个指针的类型信息,将 self 指针析构,然后判断两个类型的大小及对齐是否一致,如果一致的话,就要重新分配内存,最后调用 source 类型的 clone_from 方法来复制对象。
对于多态的对象,self 和 source 都有相同的基类,也就是 Dynbox 的模板参数 T,但可能 self 和 source 所指向的实际类型并不相同,因为我们要用 source 对象来重新初始化 self 对象,所以我们要用 source 类型的 clone_from 方法。
clone_from 通常我们不需要实现,作为一种优化,clone_from 在某些情况下省去了重新分配内存的开销,同时如果类型本身提供了优化的 clone_from 方法,也能够被调用到。
所有类都会实现 Clone Trait
这里还有一个问题,就是如果 TypeInfo 的 clone 和 clone_from 成员为 None,就会引发运行时崩溃,我们没有做更进一步的处理。这样的情况怎么会发生呢?只有类型 T 实现了 Clone Trait,我们才会为 DynBox
因此,这里有一个隐性的要求,如果基类实现了 Clone Trait,那么子类必须也实现 Clone Trait。否则智能指针 clone 方法不能保能正常工作。为简单起见,我为所有的类都实现了 Clone Trait,当然会有开销,待以后再去优化。
代码完成,我们来验证一下:
fn test_clone_ptr()
{
let mut v = Vec::<DynBox<Base>>::new();
v.push(DynBox::new(Base::new(1, 2)));
v.push(DynBox::new(Derive1::new(3, 4, 5)));
v.push(DynBox::new(Derive2::new(6, 7, 8)));
v.push(clone_and_add!(v[0], 100, 100));
v.push(clone_and_add!(v[1], 200, 200));
v.push(clone_and_add!(v[2], 300, 300));
for b in &v
{
print_address_and_data(&b);
}
}首先,我们创建三个智能指针,分别指向 Base、Derive1 和 Derive2 三个类的对象,创建副本,并输出对象地址、虚表指针和数据,结果如下:
the address = 0x7f0820000ce0, vptr = 0x5567e3435b58, data = (1, 2, -1).
the address = 0x7f0820000d00, vptr = 0x5567e3435b98, data = (3, 4, 5).
the address = 0x7f0820000d20, vptr = 0x5567e3435be0, data = (6, 7, 8).
the address = 0x7f0820000d40, vptr = 0x5567e3435b58, data = (101, 102, -1).
the address = 0x7f0820000d60, vptr = 0x5567e3435b98, data = (203, 204, 5).
the address = 0x7f0820000dd0, vptr = 0x5567e3435be0, data = (306, 307, 8).
Base::drop.
Derive1::drop.
Base::drop.
Derive2::drop.
Derive1::drop.
Base::drop.
Base::drop.
Derive1::drop.
Base::drop.
Derive2::drop.
Derive1::drop.
Base::drop.我们看到栈上分配的内存地址不够连贯,也许和 Rust 的内存分配策略有关,但对于小对象,内存浪费的现象会比较严重,也可以自行实现内存池来优化小对象的内存分配。当然这不是我们现在要讨论的。
对象析构的顺序是按对象在 Vec 中存储的顺序来的。验证成功。
现在,类和智能指针都支持了深度复制。实用性已经进一步提升了。
安全隐患
但这里还有一个问题没有解决,如果用引用调用 clone 方法,则不会触发虚机制,如下:
fn test_clone(b: &Base)
{
let b2 = b.clone();
b.func1(...); // b 可能是 Base 类对象也可能是 Base 的派生类对象
b2.func1(...); // b2 是 Base 类对象。因此两个 func1 方法可能有不同的行为。
}
let b = DynBox::<Base>::new(Derive::new(...));
test_clone(&b);在函数 test_clone 中,用户本来希望用对象的副本来做一些事情,但是副本和原始对象的类型不同,因而会有不同的行为。从而导致不可察觉的错误发生。
到这里,类功能的实现已经接近尾声了。但还有一个很重要的语法我们没有支持,下一节我们来处理这件事。