背景
开发时用到rust的特征对象来实现多态功能,遇到一个编译无法通过的问题
原始代码
trait MT {
fn run(self: Box<Self>) -> Box<dyn MT> {
let t: Box<dyn MT> = self;
t
}
}
struct C1();
impl MT for C1 {}
struct C2();
impl MT for C2 {}
struct C3();
impl MT for C3 {
fn run(self: Box<Self>) -> Box<dyn MT> {
Box::new(C1())
}
}
编译报错:
3 | let t: Box<dyn MT> = self;
| ^^^^ doesn't have a size known at compile-time
|
= note: required for the cast from `Box<Self>` to `Box<dyn MT>`
help: consider further restricting `Self`
|
2 | fn run(self: Box<Self>) -> Box<dyn MT> where Self: Sized {
| +++++++++++++++++
代码修改
但如果我把trait里run方法的默认实现去掉(关键),并将实现下发到子类编译会通过。代码如下:
trait MT {
fn run(self: Box<Self>) -> Box<dyn MT>; // 不再提供默认实现
}
struct C1();
impl MT for C1 {
fn run(self: Box<Self>) -> Box<dyn MT> {
let t: Box<dyn MT> = self;
t
}
}
struct C2();
impl MT for C2 {
fn run(self: Box<Self>) -> Box<dyn MT> { // 将原先的默认实现分别在子类中独立实现
let t: Box<dyn MT> = self;
t
}
}
struct C3();
impl MT for C3 {
fn run(self: Box<Self>) -> Box<dyn MT> { // 将原先的默认实现分别在子类中独立实现
Box::new(C1())
}
}
分析
两点说明:
- trait的默认实现只会编译一次,不会在每个子类中重新编译一次(猜测)。如果子类没有重载trait的默认实现方法,则会复用默认实现
- Box<Self>的编译时大小是已知的——指向堆上Self的指针大小(在64位机器上为8byte)。
问题1:
为什么let t: Box<dyn MT> = self
在trait的默认方法里无法通过编译,而如果是在具体的子类实现中可以通过编译?
分析:关键就在于trait的默认实现是不和任何具体子类绑定的,这样在trait的默认实现中Self具体类型是不确定的(而在具体子类实现中Self是确定的)。
在trait的默认实现中只知道栈上指向Self的指针,而无其他具体信息。那为什么无法像c++子类指针一样直接向上cast为父类指针呢?
我的理解是c++的class和rust的struct在vptr上的实现不同,因为c++的继承和rust的trait实现是两种不同的东西:
- 在c++的继承体系中,以单继承为主(多重继承有特殊的实现),每个实现virtual函数的class指针均有一个vptr指向自己的vtable,而子类的内存布局结构通常是在父类结构基础上的扩展(试具体实现),这样一个子类往往很容易cast为父类的指针形式
- 在rust中没有继承的概念,通常是以trait的方式来实现多态,同一个struct可以实现多个trait。这样的话,同一个struct可能包含多个vptr,在不知道struct的具体类型信息前是无法知道某个trait对应vptr所在的具体地址。
根据上边的分析,可知trait的默认实现里,self(注:持有所有权而非借用的self)是无法动态转变为dyn MT
的。而子类因为知道self的具体类型,进而知道其内存布局,所以可以完成动态转变。
问题2:
但为什么在trait的默认实现里可以调用其他vtable里的方发?这时候的vptr是如何得知的?
分析:我猜测在trait的默认实现方法中调用其它方法是通过偏移(offset)来实现的,因此也是不知道Self的实现该trait的vptr的。
结论:
在编译时,trait中的Box<Self>和struct里的Box<Self>的表现不完全一样:
- 在trait中,Box<Self>包含的主要信息主要是指向Self的指针和各个方法间的偏离
- 在struct中,Box<Self>包含指向Self的指针和Self的内存布局详情
进一步,Box<Self>的结论可以推广到&self、&mut self和self。这些类型的self在trait中均无法动态转换为响应的trait。例如:
trait MT {
fn run(&self) {
let t: &dyn MT = self;
}
}
仍会报错。原因也是在trait里的self只有指针信息和方法偏移信息,而无其他信息可以获取到。
额外说明
在rust里,Box<T>和Box<dyn MT>的栈上大小是不一样的。
- Box<T>只包含一个指向堆上对象的指针
- Box<dyn MT>除了包含指向堆上对象的指针外,还包含一个指向MT trait的vptr
println!("size of Box<C1>: {}", std::mem::size_of::<Box<C1>>());
println!("size of Box<dyn MT>: {}", std::mem::size_of::<Box<dyn MT>>());
// 64位操作系统输出
// size of Box<C1>: 8
// size of Box<dyn MT>: 16
Box<dyn MT>的vptr可以通过下面方法拿到
#![feature(ptr_metadata)]
let mt: Box<dyn MT> = Box::new(C1());
println!("metadata: {:?}", std::ptr::metadata(&*mt));
或者直接根据ABI,自己来获取
use std::ptr::addr_of;
let mt: Box<dyn MT> = Box::new(C1());
let raw_ptr = addr_of!(mt) as *const(); // 指向具体数据的指针
let ptr_vptr = (raw_ptr as usize + 8) as *const(); // vptr存储位置。+8是因为在64位系统上
let vptr = *(ptr_vptr as *const *const ());
println!("vptr: {:p}", vptr);