背景
開發(fā)時(shí)用到rust的特征對象來實(shí)現(xiàn)多態(tài)功能,遇到一個(gè)編譯無法通過的問題
原始代碼
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())
}
}
編譯報(bào)錯(cuò):
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方法的默認(rèn)實(shí)現(xiàn)去掉(關(guān)鍵),并將實(shí)現(xiàn)下發(fā)到子類編譯會通過。代碼如下:
trait MT {
fn run(self: Box<Self>) -> Box<dyn MT>; // 不再提供默認(rèn)實(shí)現(xiàn)
}
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> { // 將原先的默認(rèn)實(shí)現(xiàn)分別在子類中獨(dú)立實(shí)現(xiàn)
let t: Box<dyn MT> = self;
t
}
}
struct C3();
impl MT for C3 {
fn run(self: Box<Self>) -> Box<dyn MT> { // 將原先的默認(rèn)實(shí)現(xiàn)分別在子類中獨(dú)立實(shí)現(xiàn)
Box::new(C1())
}
}
分析
兩點(diǎn)說明:
- trait的默認(rèn)實(shí)現(xiàn)只會編譯一次,不會在每個(gè)子類中重新編譯一次(猜測)。如果子類沒有重載trait的默認(rèn)實(shí)現(xiàn)方法,則會復(fù)用默認(rèn)實(shí)現(xiàn)
- Box<Self>的編譯時(shí)大小是已知的——指向堆上Self的指針大?。ㄔ?4位機(jī)器上為8byte)。
問題1:
為什么let t: Box<dyn MT> = self在trait的默認(rèn)方法里無法通過編譯,而如果是在具體的子類實(shí)現(xiàn)中可以通過編譯?
分析:關(guān)鍵就在于trait的默認(rèn)實(shí)現(xiàn)是不和任何具體子類綁定的,這樣在trait的默認(rèn)實(shí)現(xiàn)中Self具體類型是不確定的(而在具體子類實(shí)現(xiàn)中Self是確定的)。
在trait的默認(rèn)實(shí)現(xiàn)中只知道棧上指向Self的指針,而無其他具體信息。那為什么無法像c++子類指針一樣直接向上cast為父類指針呢?
我的理解是c++的class和rust的struct在vptr上的實(shí)現(xiàn)不同,因?yàn)閏++的繼承和rust的trait實(shí)現(xiàn)是兩種不同的東西:
- 在c++的繼承體系中,以單繼承為主(多重繼承有特殊的實(shí)現(xiàn)),每個(gè)實(shí)現(xiàn)virtual函數(shù)的class指針均有一個(gè)vptr指向自己的vtable,而子類的內(nèi)存布局結(jié)構(gòu)通常是在父類結(jié)構(gòu)基礎(chǔ)上的擴(kuò)展(試具體實(shí)現(xiàn)),這樣一個(gè)子類往往很容易cast為父類的指針形式
- 在rust中沒有繼承的概念,通常是以trait的方式來實(shí)現(xiàn)多態(tài),同一個(gè)struct可以實(shí)現(xiàn)多個(gè)trait。這樣的話,同一個(gè)struct可能包含多個(gè)vptr,在不知道struct的具體類型信息前是無法知道某個(gè)trait對應(yīng)vptr所在的具體地址。
根據(jù)上邊的分析,可知trait的默認(rèn)實(shí)現(xiàn)里,self(注:持有所有權(quán)而非借用的self)是無法動態(tài)轉(zhuǎn)變?yōu)?code>dyn MT的。而子類因?yàn)橹纒elf的具體類型,進(jìn)而知道其內(nèi)存布局,所以可以完成動態(tài)轉(zhuǎn)變。
問題2:
但為什么在trait的默認(rèn)實(shí)現(xiàn)里可以調(diào)用其他vtable里的方發(fā)?這時(shí)候的vptr是如何得知的?
分析:我猜測在trait的默認(rèn)實(shí)現(xiàn)方法中調(diào)用其它方法是通過偏移(offset)來實(shí)現(xiàn)的,因此也是不知道Self的實(shí)現(xiàn)該trait的vptr的。
結(jié)論:
在編譯時(shí),trait中的Box<Self>和struct里的Box<Self>的表現(xiàn)不完全一樣:
- 在trait中,Box<Self>包含的主要信息主要是指向Self的指針和各個(gè)方法間的偏離
- 在struct中,Box<Self>包含指向Self的指針和Self的內(nèi)存布局詳情
進(jìn)一步,Box<Self>的結(jié)論可以推廣到&self、&mut self和self。這些類型的self在trait中均無法動態(tài)轉(zhuǎn)換為響應(yīng)的trait。例如:
trait MT {
fn run(&self) {
let t: &dyn MT = self;
}
}
仍會報(bào)錯(cuò)。原因也是在trait里的self只有指針信息和方法偏移信息,而無其他信息可以獲取到。
額外說明
在rust里,Box<T>和Box<dyn MT>的棧上大小是不一樣的。
- Box<T>只包含一個(gè)指向堆上對象的指針
- Box<dyn MT>除了包含指向堆上對象的指針外,還包含一個(gè)指向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位操作系統(tǒng)輸出
// 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));
或者直接根據(jù)ABI,自己來獲取
use std::ptr::addr_of;
let mt: Box<dyn MT> = Box::new(C1());
let raw_ptr = addr_of!(mt) as *const(); // 指向具體數(shù)據(jù)的指針
let ptr_vptr = (raw_ptr as usize + 8) as *const(); // vptr存儲位置。+8是因?yàn)樵?4位系統(tǒng)上
let vptr = *(ptr_vptr as *const *const ());
println!("vptr: {:p}", vptr);