揭開智能指針 Box 的神祕面紗

熟悉 c++ 的肯定知道 shared_ptr, unique_ptr, 而 Rust 也有智能指針 Box, Rc, Arc, RefCell 等等,本文分享 Box 底層實現

Box<T> 會在堆上分配空間,存儲 T 值,並返回對應的指針。同時 Box 也實現了 trait Deref 解引用和 Drop 析構,當 Box 離開作用域時自動釋放空間

入門例子

例子來自 the rust book, 爲了演示方便,去掉打印語句

fn main() {
    let _ = Box::new(0x11223344);
}

將變量 0x11223344 分配在堆上,所謂的裝箱,java 同學肯定很熟悉。讓我們掛載 docker, 使用 rust-gdb 查看彙編實現

Dump of assembler code for function hello_cargo::main:
   0x000055555555bdb0 <+0>: sub    $0x18,%rsp
   0x000055555555bdb4 <+4>: movl   $0x11223344,0x14(%rsp)
=> 0x000055555555bdbc <+12>: mov    $0x4,%esi
   0x000055555555bdc1 <+17>: mov    %rsi,%rdi
   0x000055555555bdc4 <+20>: callq  0x55555555b5b0 <alloc::alloc::exchange_malloc>
   0x000055555555bdc9 <+25>: mov    %rax,%rcx
   0x000055555555bdcc <+28>: mov    %rcx,%rax
   0x000055555555bdcf <+31>: movl   $0x11223344,(%rcx)
   0x000055555555bdd5 <+37>: mov    %rax,0x8(%rsp)
   0x000055555555bdda <+42>: lea    0x8(%rsp),%rdi
   0x000055555555bddf <+47>: callq  0x55555555bd20 <core::ptr::drop_in_place<alloc::boxed::Box<i32>>>
   0x000055555555bde4 <+52>: add    $0x18,%rsp
   0x000055555555bde8 <+56>: retq
End of assembler dump.

關鍵點就兩條,alloc::alloc::exchange_malloc 在堆上分配內存空間,然後將 0x11223344 存儲到這個 malloc 的地址上

函數結束時,將地址傳遞給 core::ptr::drop_in_place 去釋放,因爲編譯器知道類型是 alloc::boxed::Box<i32>, 會掉用 Box 相應的 drop 函數

單純的看這個例子,****Box 並不神祕,對應彙編實現,和普通指針沒區別,一切約束都是編譯期行爲

所有權

fn main() {
    let x = Box::new(String::from("Rust"));
    let y = *x;
    println!("x is {}", x);
}

這個例子中將字符串裝箱,其實沒必要這麼寫,因爲 String 廣義來講本身就是一種智能指針。這個例子會報錯

|     let y = *x;
  |             -- value moved here
4 |     println!("x is {}", x);
  |                         ^ value borrowed here after move

*x 解引用後對應 String, 賦值給 y 時執行 move 語義,所有權不在了,所以後續 println 不能打印 x

let y = &*x;

可以取字符串的不可變引用來 fix

底層實現

pub struct Box<
    T: ?Sized,
    #[unstable(feature = "allocator_api", issue = "32838")] A: Allocator = Global,
>(Unique<T>, A);

上面是 Box 的定義,可以看到是一個元組結構體,有兩個泛型參數:T 代表任意類型,A 代表內存分配器。標準庫裏 A 是 Gloal 默認值。其中 T 有一個泛型約束 ?Sized, 表示在編譯時可能知道類型大小,也可能不知道,當然一般都用於不知道大小的場景,很少像上文一樣存儲 int

#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<#[may_dangle] T: ?Sized, A: Allocator> Drop for Box<T, A> {
    fn drop(&mut self) {
        // FIXME: Do nothing, drop is currently performed by compiler.
    }
}

這是 Drop 實現,源碼裏也說了,由編譯器實現

#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized, A: Allocator> Deref for Box<T, A> {
    type Target = T;

    fn deref(&self) -> &{
        &**self
    }
}

#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized, A: Allocator> DerefMut for Box<T, A> {
    fn deref_mut(&mut self) -> &mut T {
        &mut **self
    }
}

實現了 Deref 可以定義解引用行爲,DerefMut 可變解引用。所以 *x 對應着操作 *(x.deref())

適用場景

官網提到以下三個場景,本質上 Box 和普通指針區別不大,所以用處不如 Rc, Arc, RefCell

官網有一個鏈表的實現

enum List {
    Cons(i32, List),
    Nil,
}

上面代碼是無法運行的,道理也很簡單,這是一種遞歸定義。對應 c 代碼也是不行的,我們一般要給 next 類型定義成指針纔行

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}

官網給的解決方案,就是將 next 變成了指針 Box<List>, 算是常識吧,沒什麼好說的

公衆號

小結

寫文章不容易,如果對大家有所幫助和啓發,請大家幫忙點擊在看點贊分享 三連

關於 Box 大家有什麼看法,歡迎留言一起討論,大牛多留言 ^_^

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/-B0PD2kKGZB2RjxeIih4_A