記一次rust動態(tài)分發(fā)分析

背景

開發(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)說明:

  1. trait的默認(rèn)實(shí)現(xiàn)只會編譯一次,不會在每個(gè)子類中重新編譯一次(猜測)。如果子類沒有重載trait的默認(rèn)實(shí)現(xiàn)方法,則會復(fù)用默認(rèn)實(shí)現(xiàn)
  2. 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);
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 《rust book 2》中介紹了一些基礎(chǔ)的知識點(diǎn),例如:引用, 借用, 泛型等等。另外,還有一些平時(shí)接觸較少,例...
    羊陸之交閱讀 1,433評論 0 4
  • 你可以把把本文作為: 對標(biāo)準(zhǔn)庫某一部分的研究 一份高級錯(cuò)誤管理指南 一個(gè)美觀的 API 設(shè)計(jì)案例 閱讀本文需要對 ...
    聯(lián)旺閱讀 1,211評論 0 1
  • Rust 語言部分細(xì)節(jié) 以大見小 - Rust快速實(shí)踐(一)- 主觀感受[https://www.jianshu....
    卷邊芝士閱讀 1,852評論 0 1
  • 1 泛型范式 C++、JAVA泛型范式有非常廣泛的應(yīng)用,也即模版方法和模版類,我們使用非常熟悉。模版方法和模版類會...
    Wu杰語閱讀 222評論 0 0
  • 科學(xué)無非就是在自然界的多樣性中尋求統(tǒng)一性(或者更確切地說,是在我們經(jīng)驗(yàn)的多樣性中尋求統(tǒng)一性)。用 Coleridg...
    草帽lufei閱讀 282評論 0 0

友情鏈接更多精彩內(nèi)容