互联网资讯 / 人工智能 · 2023年12月18日

智能指针 Box 的揭秘

对于熟悉 C++ 的开发者来说,可能对 smart pointers 中的 shared_ptr 和 unique_ptr 不陌生。而在 Rust 中,也有智能指针如 Box、Rc、Arc、RefCell 等。本文将深入探讨 Box 的底层实现。

Box 在堆上分配内存以存储 T 类型的值,并返回相应的指针。此外,Box 还实现了 Trait DeRef 用于解引用以及 Drop 的析构功能,使得 Box 在离开作用域时能够自动释放内存。

下面是一个入门示例。

这个例子源自 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 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 > 0x000055555555bde4 <+52>: add $0x18,%rsp 0x000055555555bde8 <+56>: retq End of assembler dump.

关键的过程有两步:首先是 alloc::alloc::exchange_malloc 在堆上分配内存,其次将 0x11223344 存储到该内存地址中。

当函数结束时,地址将传递给 core::ptr::drop_in_place 进行释放。由于编译器已知类型为 alloc::boxed::Box,因此会调用相应的 Drop 函数。

从这个例子来看,Box 的实现并不复杂,与普通指针的汇编实现没有太大区别,所有约束均为编译期行为。

关于所有权的示例:

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

这个例子中将字符串装箱,实际上没有必要这样做,因为 String 本身就可以视为一种智能指针。运行时将会出现错误。

*x 解引用后会得到 String,当赋值给 y 时,执行了移动语义,导致 x 的所有权转移,因此后续的 println 不能再打印 x。

let y = &*x;

可以通过获取字符串的不可变引用来修正这个问题。

底层实现如下:

pub struct Box(Unique, A);

上述代码是 Box 的定义,可以看到它是一个元组结构体,包含两个泛型参数:T 代表任意类型,A 代表内存分配器。在标准库中,A 的默认值是 Global。T 具有一个泛型约束 ?Sized,表示在编译时可能知道类型的大小,也可能不知道,通常用于不知道大小的场景。很少像前面提到的那样存储整数。

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

这是 Drop 的实现,源码中指出该功能由编译器处理。

impl Deref for Box { type Target = T; fn deref(&self) -> &T { &**self } } impl DerefMut for Box { 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), 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,这是一种常识,无需多言。