rust - Rust 如何知道在堆栈展开期间是否运行析构函数?

标签 rust destructor undefined-behavior stack-unwinding

mem::uninitialized 的文档指出为什么使用该函数是危险/不安全的:在未初始化的内存上调用 drop 是未定义的行为。

所以我认为这段代码应该是未定义的:

let a: TypeWithDrop = unsafe { mem::uninitialized() };
panic!("=== Testing ==="); // Destructor of `a` will be run (U.B)

但是,我编写的这段代码可以在安全的 Rust 中运行,并且似乎不会出现未定义的行为:

#![feature(conservative_impl_trait)]

trait T {
    fn disp(&mut self);
}

struct A;
impl T for A {
    fn disp(&mut self) { println!("=== A ==="); }
}
impl Drop for A {
    fn drop(&mut self) { println!("Dropping A"); }
}

struct B;
impl T for B {
    fn disp(&mut self) { println!("=== B ==="); }
}
impl Drop for B {
    fn drop(&mut self) { println!("Dropping B"); }
}

fn foo() -> impl T { return A; }
fn bar() -> impl T { return B; }

fn main() {
    let mut a;
    let mut b;

    let i = 10;
    let t: &mut T = if i % 2 == 0 {
        a = foo();
        &mut a
    } else {
        b = bar();
        &mut b
    };

    t.disp();
    panic!("=== Test ===");
}

它似乎总是执行正确的析构函数,而忽略另一个。如果我尝试使用 ab(例如 a.disp() 而不是 t.disp())它正确地错误地指出我可能正在使用未初始化的内存。令我惊讶的是,当 panicking 时,无论 i 的值是多少,它总是运行正确的析构函数(打印预期的字符串)。

这是怎么发生的?如果运行时可以确定要运行哪个析构函数,是否应该从 mem::uninitialized() 的文档中删除有关强制需要为实现了 Drop 的类型初始化内存的部分如上链接?

最佳答案

使用 drop flags .

Rust(直到并包括版本 1.12)在其类型实现 Drop 的每个值中存储一个 bool 标志(因此将该类型的大小增加一个字节)。该标志决定是否运行析构函数。因此,当您执行 b = bar() 时,它会为 b 变量设置标志,因此只运行 b 的析构函数。反之亦然,a

请注意,从 Rust 版本 1.13(在编写 beta 编译器时)开始,该标志不再存储在类型中,而是存储在每个变量或临时变量的堆栈中。 Rust 编译器中 MIR 的出现使这成为可能。 MIR 显着简化了 Rust 代码到机器代码的翻译,从而使该功能能够将丢弃标志移动到堆栈。如果优化可以在编译时确定何时删除哪个对象,则通常会消除该标志。

您可以通过查看类型的大小在 1.12 版之前的 Rust 编译器中“观察”此标志:

struct A;

struct B;

impl Drop for B {
    fn drop(&mut self) {}
}

fn main() {
    println!("{}", std::mem::size_of::<A>());
    println!("{}", std::mem::size_of::<B>());
}

在堆栈标志之前分别打印 01,在堆栈标志之前打印 00

但是,使用 mem::uninitialized 仍然不安全,因为编译器仍然看到对 a 变量的赋值并设置丢弃标志。因此析构函数将在未初始化的内存上调用。请注意,在您的示例中, Drop impl 不会访问您类型的任何内存(除了 drop 标志,但您不可见)。因此,您没有访问未初始化的内存(无论如何,它的大小为零字节,因为您的类型是零大小的结构)。据我所知,这意味着您的 unsafe { std::mem::uninitialized() } 代码实际上是安全的,因为之后不会发生内存不安全。

关于rust - Rust 如何知道在堆栈展开期间是否运行析构函数?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39750841/

相关文章:

rust - 为什么传递参数时 fmt::Arguments as_str 不起作用?

c++ - 构造和破坏的顺序

c++ - 如果我在析构函数中创建一个对象,会发生什么?

c++ - std::underlying_type :SFINAE 是否可以防止未定义的行为?

C++ - 以下代码会导致未定义的行为吗?

rust - 在 Rust 中具有变量阴影的堆栈分配内存会发生什么?

rust - 为什么需要导入特征以使用它为类型定义的方法?

macros - 将 Rust 宏类型转换为表达式

c++ - 析构函数导致段错误

c++ - 读取不确定值是未定义的行为吗?