Rust 中涉及临时对象的销毁顺序
Destruction order involving temporaries in Rust
在 C++ 中(如果错误请纠正我),通过常量引用的临时绑定应该比它绑定到的表达式更有效。我假设在 Rust 中也是如此,但在两种不同的情况下我得到了两种不同的行为。
考虑:
struct A;
impl Drop for A { fn drop(&mut self) { println!("Drop A.") } }
struct B(*const A);
impl Drop for B { fn drop(&mut self) { println!("Drop B.") } }
fn main() {
let _ = B(&A as *const A); // B is destroyed after this expression itself.
}
输出为:
Drop B.
Drop A.
这就是您所期望的。但现在如果你这样做:
fn main() {
let _b = B(&A as *const A); // _b will be dropped when scope exits main()
}
输出为:
Drop A.
Drop B.
这不是我所期望的。
这是有意为之吗?如果是这样,那么这两种情况下行为差异的基本原理是什么?
我正在使用 Rust 1.12.1。
原始指针本身没有任何类型的生命周期,因此编译器可能会做这样的事情:
示例:
- B 已创建(以便它可以在其中容纳一个
*const A
)
- 创建了一个
- B 未绑定到绑定,因此被丢弃
- A 不需要,因此被丢弃
让我们看看 MIR:
fn main() -> () {
let mut _0: (); // return pointer
let mut _1: B;
let mut _2: *const A;
let mut _3: *const A;
let mut _4: &A;
let mut _5: &A;
let mut _6: A;
let mut _7: ();
bb0: {
StorageLive(_1); // scope 0 at <anon>:8:13: 8:30
StorageLive(_2); // scope 0 at <anon>:8:15: 8:29
StorageLive(_3); // scope 0 at <anon>:8:15: 8:17
StorageLive(_4); // scope 0 at <anon>:8:15: 8:17
StorageLive(_5); // scope 0 at <anon>:8:15: 8:17
StorageLive(_6); // scope 0 at <anon>:8:16: 8:17
_6 = A::A; // scope 0 at <anon>:8:16: 8:17
_5 = &_6; // scope 0 at <anon>:8:15: 8:17
_4 = &(*_5); // scope 0 at <anon>:8:15: 8:17
_3 = _4 as *const A (Misc); // scope 0 at <anon>:8:15: 8:17
_2 = _3; // scope 0 at <anon>:8:15: 8:29
_1 = B::B(_2,); // scope 0 at <anon>:8:13: 8:30
drop(_1) -> bb1; // scope 0 at <anon>:8:31: 8:31
}
bb1: {
StorageDead(_1); // scope 0 at <anon>:8:31: 8:31
StorageDead(_2); // scope 0 at <anon>:8:31: 8:31
StorageDead(_3); // scope 0 at <anon>:8:31: 8:31
StorageDead(_4); // scope 0 at <anon>:8:31: 8:31
StorageDead(_5); // scope 0 at <anon>:8:31: 8:31
drop(_6) -> bb2; // scope 0 at <anon>:8:31: 8:31
}
bb2: {
StorageDead(_6); // scope 0 at <anon>:8:31: 8:31
_0 = (); // scope 0 at <anon>:7:11: 9:2
return; // scope 0 at <anon>:9:2: 9:2
}
}
我们可以看到 drop(_1)
确实在 drop(_6)
之前被调用,因此你得到上面的输出。
- 例子
在此示例中,B 绑定到绑定
- 创建了B(同上原因)
- 创建了一个
- A 未绑定并被丢弃
- B 超出范围并被丢弃
对应的MIR:
fn main() -> () {
let mut _0: (); // return pointer
scope 1 {
let _1: B; // "b" in scope 1 at <anon>:8:9: 8:10
}
let mut _2: *const A;
let mut _3: *const A;
let mut _4: &A;
let mut _5: &A;
let mut _6: A;
let mut _7: ();
bb0: {
StorageLive(_1); // scope 0 at <anon>:8:9: 8:10
StorageLive(_2); // scope 0 at <anon>:8:15: 8:29
StorageLive(_3); // scope 0 at <anon>:8:15: 8:17
StorageLive(_4); // scope 0 at <anon>:8:15: 8:17
StorageLive(_5); // scope 0 at <anon>:8:15: 8:17
StorageLive(_6); // scope 0 at <anon>:8:16: 8:17
_6 = A::A; // scope 0 at <anon>:8:16: 8:17
_5 = &_6; // scope 0 at <anon>:8:15: 8:17
_4 = &(*_5); // scope 0 at <anon>:8:15: 8:17
_3 = _4 as *const A (Misc); // scope 0 at <anon>:8:15: 8:17
_2 = _3; // scope 0 at <anon>:8:15: 8:29
_1 = B::B(_2,); // scope 0 at <anon>:8:13: 8:30
StorageDead(_2); // scope 0 at <anon>:8:31: 8:31
StorageDead(_3); // scope 0 at <anon>:8:31: 8:31
StorageDead(_4); // scope 0 at <anon>:8:31: 8:31
StorageDead(_5); // scope 0 at <anon>:8:31: 8:31
drop(_6) -> [return: bb3, unwind: bb2]; // scope 0 at <anon>:8:31: 8:31
}
bb1: {
resume; // scope 0 at <anon>:7:1: 9:2
}
bb2: {
drop(_1) -> bb1; // scope 0 at <anon>:9:2: 9:2
}
bb3: {
StorageDead(_6); // scope 0 at <anon>:8:31: 8:31
_0 = (); // scope 1 at <anon>:7:11: 9:2
drop(_1) -> bb4; // scope 0 at <anon>:9:2: 9:2
}
bb4: {
StorageDead(_1); // scope 0 at <anon>:9:2: 9:2
return; // scope 0 at <anon>:9:2: 9:2
}
}
正如我们所见,drop(_6)
确实在 drop(_1)
之前被调用,所以我们得到了您所看到的行为。
在语句末尾删除临时变量,就像在 C++ 中一样。然而,IIRC,Rust 中的销毁顺序是未指定的(我们将在下面看到它的后果),尽管当前的实现似乎只是以相反的构造顺序丢弃值。
let _ = x;
和let _b = x;
有很大区别。 _
不是 Rust 中的标识符:它是一个通配符模式。由于此模式未找到任何变量,最终值实际上被丢弃在语句末尾。
另一方面,_b
是一个标识符,因此该值被绑定到一个具有该名称的变量,从而延长其生命周期直到函数结束。但是,A
实例仍然是一个临时实例,因此它将在语句末尾被删除(我相信 C++ 也会这样做)。由于语句结束在函数结束之前,因此首先删除 A
实例,然后删除 B
实例。
为了更清楚,让我们在 main
中添加另一个语句:
fn main() {
let _ = B(&A as *const A);
println!("End of main.");
}
这会产生以下输出:
Drop B.
Drop A.
End of main.
到目前为止一切顺利。现在让我们尝试 let _b
;输出是:
Drop A.
End of main.
Drop B.
我们可以看到,在End of main.
之后打印了Drop B
。这表明 B
实例在函数结束之前一直存在,解释了不同的销毁顺序。
现在,让我们看看如果我们修改 B
以使用具有生命周期的借用指针而不是原始指针会发生什么。实际上,让我们更进一步,暂时删除 Drop
实现:
struct A;
struct B<'a>(&'a A);
fn main() {
let _ = B(&A);
}
编译正常。在幕后,Rust 为 A
实例和 B
实例分配相同的生命周期(即如果我们引用 B
实例,它的类型将是 &'a B<'a>
其中 'a
是完全相同的生命周期)。当两个值具有相同的生命周期时,那么我们必然需要先删除其中一个,并且如上所述,顺序是未指定的。如果我们加回 Drop
实现会怎样?
struct A;
impl Drop for A { fn drop(&mut self) { println!("Drop A.") } }
struct B<'a>(&'a A);
impl<'a> Drop for B<'a> { fn drop(&mut self) { println!("Drop B.") } }
fn main() {
let _ = B(&A);
}
现在我们遇到编译器错误:
error: borrowed value does not live long enough
--> <anon>:8:16
|
8 | let _ = B(&A);
| ^ does not live long enough
|
note: reference must be valid for the destruction scope surrounding statement at 8:4...
--> <anon>:8:5
|
8 | let _ = B(&A);
| ^^^^^^^^^^^^^^
note: ...but borrowed value is only valid for the statement at 8:4
--> <anon>:8:5
|
8 | let _ = B(&A);
| ^^^^^^^^^^^^^^
help: consider using a `let` binding to increase its lifetime
--> <anon>:8:5
|
8 | let _ = B(&A);
| ^^^^^^^^^^^^^^
由于 A
实例和 B
实例都被分配了相同的生命周期,Rust 无法推断出这些对象的销毁顺序。该错误来自于当 B<'a>
实现 Drop
时 Rust 拒绝使用对象本身的生命周期实例化 B<'a>
(此规则是作为 RFC 769 之前的结果添加的锈 1.0)。如果允许,drop
将能够访问已经删除的值!但是,如果 B<'a>
没有实现 Drop
,那么它是允许的,因为我们知道当结构被删除时没有代码会尝试访问 B
的字段。
在 C++ 中(如果错误请纠正我),通过常量引用的临时绑定应该比它绑定到的表达式更有效。我假设在 Rust 中也是如此,但在两种不同的情况下我得到了两种不同的行为。
考虑:
struct A;
impl Drop for A { fn drop(&mut self) { println!("Drop A.") } }
struct B(*const A);
impl Drop for B { fn drop(&mut self) { println!("Drop B.") } }
fn main() {
let _ = B(&A as *const A); // B is destroyed after this expression itself.
}
输出为:
Drop B.
Drop A.
这就是您所期望的。但现在如果你这样做:
fn main() {
let _b = B(&A as *const A); // _b will be dropped when scope exits main()
}
输出为:
Drop A.
Drop B.
这不是我所期望的。
这是有意为之吗?如果是这样,那么这两种情况下行为差异的基本原理是什么?
我正在使用 Rust 1.12.1。
原始指针本身没有任何类型的生命周期,因此编译器可能会做这样的事情:
示例:
- B 已创建(以便它可以在其中容纳一个
*const A
) - 创建了一个
- B 未绑定到绑定,因此被丢弃
- A 不需要,因此被丢弃
- B 已创建(以便它可以在其中容纳一个
让我们看看 MIR:
fn main() -> () {
let mut _0: (); // return pointer
let mut _1: B;
let mut _2: *const A;
let mut _3: *const A;
let mut _4: &A;
let mut _5: &A;
let mut _6: A;
let mut _7: ();
bb0: {
StorageLive(_1); // scope 0 at <anon>:8:13: 8:30
StorageLive(_2); // scope 0 at <anon>:8:15: 8:29
StorageLive(_3); // scope 0 at <anon>:8:15: 8:17
StorageLive(_4); // scope 0 at <anon>:8:15: 8:17
StorageLive(_5); // scope 0 at <anon>:8:15: 8:17
StorageLive(_6); // scope 0 at <anon>:8:16: 8:17
_6 = A::A; // scope 0 at <anon>:8:16: 8:17
_5 = &_6; // scope 0 at <anon>:8:15: 8:17
_4 = &(*_5); // scope 0 at <anon>:8:15: 8:17
_3 = _4 as *const A (Misc); // scope 0 at <anon>:8:15: 8:17
_2 = _3; // scope 0 at <anon>:8:15: 8:29
_1 = B::B(_2,); // scope 0 at <anon>:8:13: 8:30
drop(_1) -> bb1; // scope 0 at <anon>:8:31: 8:31
}
bb1: {
StorageDead(_1); // scope 0 at <anon>:8:31: 8:31
StorageDead(_2); // scope 0 at <anon>:8:31: 8:31
StorageDead(_3); // scope 0 at <anon>:8:31: 8:31
StorageDead(_4); // scope 0 at <anon>:8:31: 8:31
StorageDead(_5); // scope 0 at <anon>:8:31: 8:31
drop(_6) -> bb2; // scope 0 at <anon>:8:31: 8:31
}
bb2: {
StorageDead(_6); // scope 0 at <anon>:8:31: 8:31
_0 = (); // scope 0 at <anon>:7:11: 9:2
return; // scope 0 at <anon>:9:2: 9:2
}
}
我们可以看到 drop(_1)
确实在 drop(_6)
之前被调用,因此你得到上面的输出。
- 例子
在此示例中,B 绑定到绑定
- 创建了B(同上原因)
- 创建了一个
- A 未绑定并被丢弃
- B 超出范围并被丢弃
对应的MIR:
fn main() -> () {
let mut _0: (); // return pointer
scope 1 {
let _1: B; // "b" in scope 1 at <anon>:8:9: 8:10
}
let mut _2: *const A;
let mut _3: *const A;
let mut _4: &A;
let mut _5: &A;
let mut _6: A;
let mut _7: ();
bb0: {
StorageLive(_1); // scope 0 at <anon>:8:9: 8:10
StorageLive(_2); // scope 0 at <anon>:8:15: 8:29
StorageLive(_3); // scope 0 at <anon>:8:15: 8:17
StorageLive(_4); // scope 0 at <anon>:8:15: 8:17
StorageLive(_5); // scope 0 at <anon>:8:15: 8:17
StorageLive(_6); // scope 0 at <anon>:8:16: 8:17
_6 = A::A; // scope 0 at <anon>:8:16: 8:17
_5 = &_6; // scope 0 at <anon>:8:15: 8:17
_4 = &(*_5); // scope 0 at <anon>:8:15: 8:17
_3 = _4 as *const A (Misc); // scope 0 at <anon>:8:15: 8:17
_2 = _3; // scope 0 at <anon>:8:15: 8:29
_1 = B::B(_2,); // scope 0 at <anon>:8:13: 8:30
StorageDead(_2); // scope 0 at <anon>:8:31: 8:31
StorageDead(_3); // scope 0 at <anon>:8:31: 8:31
StorageDead(_4); // scope 0 at <anon>:8:31: 8:31
StorageDead(_5); // scope 0 at <anon>:8:31: 8:31
drop(_6) -> [return: bb3, unwind: bb2]; // scope 0 at <anon>:8:31: 8:31
}
bb1: {
resume; // scope 0 at <anon>:7:1: 9:2
}
bb2: {
drop(_1) -> bb1; // scope 0 at <anon>:9:2: 9:2
}
bb3: {
StorageDead(_6); // scope 0 at <anon>:8:31: 8:31
_0 = (); // scope 1 at <anon>:7:11: 9:2
drop(_1) -> bb4; // scope 0 at <anon>:9:2: 9:2
}
bb4: {
StorageDead(_1); // scope 0 at <anon>:9:2: 9:2
return; // scope 0 at <anon>:9:2: 9:2
}
}
正如我们所见,drop(_6)
确实在 drop(_1)
之前被调用,所以我们得到了您所看到的行为。
在语句末尾删除临时变量,就像在 C++ 中一样。然而,IIRC,Rust 中的销毁顺序是未指定的(我们将在下面看到它的后果),尽管当前的实现似乎只是以相反的构造顺序丢弃值。
let _ = x;
和let _b = x;
有很大区别。 _
不是 Rust 中的标识符:它是一个通配符模式。由于此模式未找到任何变量,最终值实际上被丢弃在语句末尾。
另一方面,_b
是一个标识符,因此该值被绑定到一个具有该名称的变量,从而延长其生命周期直到函数结束。但是,A
实例仍然是一个临时实例,因此它将在语句末尾被删除(我相信 C++ 也会这样做)。由于语句结束在函数结束之前,因此首先删除 A
实例,然后删除 B
实例。
为了更清楚,让我们在 main
中添加另一个语句:
fn main() {
let _ = B(&A as *const A);
println!("End of main.");
}
这会产生以下输出:
Drop B.
Drop A.
End of main.
到目前为止一切顺利。现在让我们尝试 let _b
;输出是:
Drop A.
End of main.
Drop B.
我们可以看到,在End of main.
之后打印了Drop B
。这表明 B
实例在函数结束之前一直存在,解释了不同的销毁顺序。
现在,让我们看看如果我们修改 B
以使用具有生命周期的借用指针而不是原始指针会发生什么。实际上,让我们更进一步,暂时删除 Drop
实现:
struct A;
struct B<'a>(&'a A);
fn main() {
let _ = B(&A);
}
编译正常。在幕后,Rust 为 A
实例和 B
实例分配相同的生命周期(即如果我们引用 B
实例,它的类型将是 &'a B<'a>
其中 'a
是完全相同的生命周期)。当两个值具有相同的生命周期时,那么我们必然需要先删除其中一个,并且如上所述,顺序是未指定的。如果我们加回 Drop
实现会怎样?
struct A;
impl Drop for A { fn drop(&mut self) { println!("Drop A.") } }
struct B<'a>(&'a A);
impl<'a> Drop for B<'a> { fn drop(&mut self) { println!("Drop B.") } }
fn main() {
let _ = B(&A);
}
现在我们遇到编译器错误:
error: borrowed value does not live long enough
--> <anon>:8:16
|
8 | let _ = B(&A);
| ^ does not live long enough
|
note: reference must be valid for the destruction scope surrounding statement at 8:4...
--> <anon>:8:5
|
8 | let _ = B(&A);
| ^^^^^^^^^^^^^^
note: ...but borrowed value is only valid for the statement at 8:4
--> <anon>:8:5
|
8 | let _ = B(&A);
| ^^^^^^^^^^^^^^
help: consider using a `let` binding to increase its lifetime
--> <anon>:8:5
|
8 | let _ = B(&A);
| ^^^^^^^^^^^^^^
由于 A
实例和 B
实例都被分配了相同的生命周期,Rust 无法推断出这些对象的销毁顺序。该错误来自于当 B<'a>
实现 Drop
时 Rust 拒绝使用对象本身的生命周期实例化 B<'a>
(此规则是作为 RFC 769 之前的结果添加的锈 1.0)。如果允许,drop
将能够访问已经删除的值!但是,如果 B<'a>
没有实现 Drop
,那么它是允许的,因为我们知道当结构被删除时没有代码会尝试访问 B
的字段。