“box”关键字是什么作用?

73
在Rust中,我们可以使用Box<T>类型来在堆上分配内存。这种类型用于安全地抽象指向堆内存的指针。Box<T>由Rust标准库提供。
我对Box<T>的分配实现方式很好奇,所以我找到了它的源代码。以下是Box<T>::new的代码(截至Rust 1.0):
impl<T> Box<T> {
    /// Allocates memory on the heap and then moves `x` into it.
    /// [...]
    #[stable(feature = "rust1", since = "1.0.0")]
    #[inline(always)]
    pub fn new(x: T) -> Box<T> {
        box x
    }
}

实现中唯一的一行代码返回值为 box x。这个 box 关键字在官方文档中没有任何说明;实际上,它只是在 std::boxed 文档页面上简要提到过。
3个回答

41

注意:此回复有点旧。由于它涉及内部和不稳定的特性,事情已经有了一些变化。但基本机制仍然相同,因此该答案仍能够解释box的底层机制。

box x通常使用标记为exchange_malloc的语言项函数进行分配和标记为exchange_free的函数进行释放。您可以在默认标准库中的heap.rs#L112heap.rs#L125中看到这些的实现。

最终,box x语法取决于以下语言项:

  • 使用owned_box在一个Box结构体中封装分配的指针。这个结构体不需要Drop实现,编译器会自动实现。
  • 使用exchange_malloc来分配内存。
  • 使用exchange_free来释放之前分配的内存。

在不稳定的Rust书籍的lang items章节中可以通过这个no_std示例有效地看到这一点:

#![feature(lang_items, box_syntax, start, no_std, libc)]
#![no_std]

extern crate libc;

extern {
    fn abort() -> !;
}

#[lang = "owned_box"]
pub struct Box<T>(*mut T);

#[lang = "exchange_malloc"]
unsafe fn allocate(size: usize, _align: usize) -> *mut u8 {
    let p = libc::malloc(size as libc::size_t) as *mut u8;

    // malloc failed
    if p as usize == 0 {
        abort();
    }

    p
}
#[lang = "exchange_free"]
unsafe fn deallocate(ptr: *mut u8, _size: usize, _align: usize) {
    libc::free(ptr as *mut libc::c_void)
}

#[start]
fn main(argc: isize, argv: *const *const u8) -> isize {
    let x = box 1;

    0
}

#[lang = "stack_exhausted"] extern fn stack_exhausted() {}
#[lang = "eh_personality"] extern fn eh_personality() {}
#[lang = "panic_fmt"] fn panic_fmt() -> ! { loop {} }

请注意,Box 结构体没有实现 Drop 方法。那么我们来看一下生成的 main 的 LLVM IR:
define internal i64 @_ZN4main20hbd13b522fdb5b7d4ebaE(i64, i8**) unnamed_addr #1 {
entry-block:
  %argc = alloca i64
  %argv = alloca i8**
  %x = alloca i32*
  store i64 %0, i64* %argc, align 8
  store i8** %1, i8*** %argv, align 8
  %2 = call i8* @_ZN8allocate20hf9df30890c435d76naaE(i64 4, i64 4)
  %3 = bitcast i8* %2 to i32*
  store i32 1, i32* %3, align 4
  store i32* %3, i32** %x, align 8
  call void @"_ZN14Box$LT$i32$GT$9drop.103617h8817b938807fc41eE"(i32** %x)
  ret i64 0
}

预期调用allocate_ZN8allocate20hf9df30890c435d76naaE)来构建Box,同时...看!Box (_ZN14Box$LT$i32$GT$9drop.103617h8817b938807fc41eE) 的Drop方法!让我们看一下这个方法的IR:

define internal void @"_ZN14Box$LT$i32$GT$9drop.103617h8817b938807fc41eE"(i32**) unnamed_addr #0 {
entry-block:
  %1 = load i32** %0
  %2 = ptrtoint i32* %1 to i64
  %3 = icmp ne i64 %2, 2097865012304223517
  br i1 %3, label %cond, label %next

next:                                             ; preds = %cond, %entry-    block
  ret void

cond:                                             ; preds = %entry-block
  %4 = bitcast i32* %1 to i8*
  call void @_ZN10deallocate20he2bff5e01707ad50VaaE(i8* %4, i64 4, i64 4)
  br label %next
}

这里有一个编译器生成的 Drop,它调用了 deallocate (ZN10deallocate20he2bff5e01707ad50VaaE)。即使在标准库上,Drop 特质也不是由用户代码实现的。事实上,Box 是一个有点神奇的结构体。

18
在`box`被标记为不稳定之前,它被用作调用 `Box::new` 的简写。但是,它一直旨在能够分配任意类型,例如 `Rc`,或者使用任意的分配器。由于这些都没有最终确定,因此它在1.0版本中未被标记为稳定。这样做是为了防止支持所有 Rust 1.x 的错误决策。 有关更多信息,请参阅 更改“放置新”语法的RFC,并将其特征门控。

那么目前(在上面的实现中),box x 是放置新语法吗? - George Hilliard
1
@thirtythreeforty,是的,这是放置新语法,目前硬编码为仅与“Box”一起使用。 - Vladimir Matveev
因此,Box最终将不再像我创建的任何类型一样被特殊处理,它将简单地使用其他(1.0版本后)语言功能。 - George Hilliard

10

box 的作用和 Box::new() 相同 - 它创建一个拥有所有权的 box。

我认为你找不到 box 关键字的实现,因为它目前被硬编码为与拥有所有权的 boxes 一起工作,并且 Box 类型是语言项:

#[lang = "owned_box"]
#[stable(feature = "rust1", since = "1.0.0")]
#[fundamental]
pub struct Box<T>(Unique<T>);

由于它是语言项,编译器有特殊的逻辑来处理其实例化,可以使用 box 关键字连接。

我认为编译器将 box 分配委托给 alloc::heap 模块中的函数。

至于 box 关键字在一般情况下的作用和应该做什么,Shepmaster 的回答描述得非常完美。


1
box 所做的正是 Box::new() 所做的。这只是在理论上成立,对吧?如果我理解正确,实际上有一个差别,box 可以绕过堆栈分配,但 Box::new 不能 - bluenote10

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接