16

我在某处读到,在具有指针的语言中,由于各种原因,编译器不可能在编译时完全确定所有指针是否正确使用和/或是否有效(引用活动对象),因为那会本质上构成解决停机问题。直观地说,这并不奇怪,因为在这种情况下,我们将能够在编译时推断程序的运行时行为,类似于这个相关问题中所述的内容。

然而,据我所知,Rust 语言要求指针检查完全在编译时完成(没有与指针相关的未定义行为,至少是“安全”指针,并且没有“无效指针”或“空指针”运行时例外)。

假设 Rust 编译器没有解决停机问题,那么谬误在哪里?

  • 是不是指针检查没有完全在编译时完成,并且与 C 中的原始指针相比,Rust 的智能指针仍然引入了一些运行时开销?
  • 或者 Rust 编译器是否可能无法做出完全正确的决定,并且它有时需要 Just Trust The Programmer™,可能使用生命周期注释之一(具有<'lifetime_ident>语法的注释)?在这种情况下,这是否意味着指针/内存安全保证不是 100%,仍然依赖于程序员编写正确的代码?
  • 另一种可能性是,Rust 指针是非“通用的”或在某种意义上受到限制,因此编译器可以在编译时完全推断它们的属性,但它们不如 C 中的原始指针或 C++ 中的智能指针有用。
  • 或者也许它是完全不同的东西,我误解了一个或多个
    { "pointer", "safety", "guaranteed", "compile-time" }.
4

3 回答 3

9

免责声明:我有点着急,所以这有点曲折。随意清理它。

语言设计师讨厌的一个偷偷摸摸的把戏™ 基本上是这样的:Rust只能推理'static生命周期(用于全局变量和其他整个程序生命周期的东西)和堆栈(局部)变量的生命周期:它不能表达或推理分配的生命周期。

这意味着几件事。首先,所有处理堆分配的库类型( Box<T>, Rc<T>Arc<T>拥有它们所指向的东西。因此,它们实际上并不需要生命周期来存在。

当您访问智能指针的内容时,您确实需要生命周期。例如:

let mut x: Box<i32> = box 0;
*x = 42;

第二行的幕后情况是这样的:

{
    let box_ref: &mut Box<i32> = &mut x;
    let heap_ref: &mut i32 = box_ref.deref_mut();
    *heap_ref = 42;
}

换句话说,因为Box不是魔术,我们必须告诉编译器如何把它变成一个普通的、普通的借用指针。这就是 theDerefDerefMuttraits 的用途。这就提出了一个问题: 的生命周期究竟是heap_ref什么?

这个问题的答案是DerefMut(从记忆中,因为我很着急)的定义:

trait DerefMut {
    type Target;
    fn deref_mut<'a>(&'a mut self) -> &'a mut Target;
}

就像我之前说的,Rust绝对不能谈论“堆生命周期”。相反,它必须将堆分配i32的生命周期与它手头唯一的另一个生命周期联系起来:Box.

这意味着“复杂”的事物没有可表达的生命周期,因此必须拥有它们管理的事物。当您将复杂的智能指针/句柄转换为简单的借用指针时,必须引入生命周期,而您通常只使用句柄本身的生命周期。

实际上,我应该澄清一下:“句柄的生命周期”,我真正的意思是“当前存储句柄的变量的生命周期”:生命周期实际上是用于storage,而不是用于values。这通常是为什么 Rust 新手在无法弄清楚为什么他们不能做类似的事情时会被绊倒的原因:

fn thingy<'a>() -> (Box<i32>, &'a i32) {
    let x = box 1701;
    (x, &x)
}

“但是……我知道盒子会继续存在,为什么编译器说它没有?!” 因为 Rust 无法推断堆生命周期,因此必须将生命周期绑定&x变量 x而不是它恰好指向的堆分配。

于 2015-04-14T13:50:05.277 回答
8

是不是指针检查没有完全在编译时完成,并且与 C 中的原始指针相比,Rust 的智能指针仍然会引入一些运行时开销?

对于在编译时无法检查的内容,有特殊的运行时检查。这些通常在cell板条箱中找到。但总的来说,Rust 在编译时检查所有内容,并且应该生成与 C 中相同的代码(如果您的 C 代码没有执行未定义的内容)。

或者,Rust 编译器是否可能无法做出完全正确的决定,并且有时需要 Just Trust The Programmer™,可能使用生命周期注释之一(具有 <'lifetime_ident> 语法的注释)?在这种情况下,这是否意味着指针/内存安全保证不是 100%,仍然依赖于程序员编写正确的代码?

如果编译器不能做出正确的决定,你会得到一个编译时错误,告诉你编译器无法验证你在做什么。这也可能会限制您使用您知道正确的内容,但编译器不会。在这种情况下,你总是可以去unsafe编码。但正如您正确假设的那样,编译器部分依赖于程序员。

编译器检查函数的实现,看看它是否完全按照生命周期所说的那样做。然后,在函数的调用点,它检查程序员是否正确地使用了函数。这类似于类型检查。C++ 编译器检查您是否返回了正确类型的对象。然后它在调用站点检查返回的对象是否存储在正确类型的变量中。函数的程序员在任何时候都不能违背承诺(除非unsafe使用了,但您始终可以让编译器强制unsafe在您的项目中使用 no)

Rust 不断改进。一旦编译器变得更聪明,更多的东西可能在 Rust 中变得合法。

另一种可能性是,Rust 指针不是“通用的”或在某种意义上受到限制,因此编译器可以在编译时完全推断它们的属性,但它们不如 C 中的原始指针或 C++ 中的智能指针有用。

在 C 语言中有一些可能出错的地方:

  1. 悬空指针
  2. 双免费
  3. 空指针
  4. 野指针

这些不会发生在安全的 Rust 中。

  1. 您永远不能拥有一个指向不再位于堆栈或堆上的对象的指针。这在编译时通过生命周期得到证明。
  2. Rust 中没有手动内存管理。使用 aBox分配对象(类似于但不等于unique_ptrC++ 中的 a)
  3. 同样,没有手动内存管理。Boxes 自动释放内存。
  4. 在安全的 Rust 中,您可以创建指向任何位置的指针,但不能取消引用它。您创建的任何引用始终绑定到一个对象。

在 C++ 中有一些可能出错的地方:

  1. C中可能出错的一切
  2. SmartPointers 只帮助您不要忘记调用free。您仍然可以创建悬空引用:auto x = make_unique<int>(42); auto& y = *x; x.reset(); y = 99;

Rust 修复了这些:

  1. 看上面
  2. 只要y存在,就不能修改x. 这是在编译时检查的,不能被更多级别的间接或结构规避。

我在某处读到,在具有指针的语言中,由于各种原因,编译器不可能在编译时完全确定所有指针是否正确使用和/或是否有效(引用活动对象),因为那会本质上构成解决停机问题。

Rust 并不能证明你所有的指针都被正确使用了。你仍然可以编写虚假程序。Rust 证明你没有使用无效指针。Rust 证明你永远不会有空指针。Rust 证明你永远不会有两个指向同一个对象的指针,除非所有这些指针都是不可变的(const)。Rust 不允许您编写任何程序(因为这将包括违反内存安全的程序)。现在 Rust 仍然阻止你编写一些有用的程序,但有计划允许更多(合法的)程序用安全的 Rust 编写。

直观地说,这并不奇怪,因为在这种情况下,我们将能够在编译时推断程序的运行时行为,类似于这个相关问题中所述的内容。

重新访问您引用的关于停止问题的问题中的示例:

void foo() {
    if (bar() == 0) this->a = 1;
}

上面的 C++ 代码看起来是 Rust 中的两种方式之一:

fn foo(&mut self) {
    if self.bar() == 0 {
        self.a = 1;
    }
}

fn foo(&mut self) {
    if bar() == 0 {
        self.a = 1;
    }
}

对于任意的bar,您无法证明这一点,因为它可能会访问全局状态。Rust 很快就获得了const函数,可用于在编译时计算东西(类似于constexpr)。If baris const,证明 if在编译时self.a设置为微不足道。1除此之外,没有pure功能或功能内容的其他限制,您永远无法证明是否self.a设置为1

Rust 目前并不关心你的代码是否被调用。它关心self.a分配期间的内存是否仍然存在。self.bar()永远不能破坏selfunsafe代码除外)。因此,在分支self.a内将始终可用。if

于 2015-04-14T14:06:18.623 回答
2

Rust 引用的大部分安全性由严格的规则保证:

  • 如果你拥有一个 const 引用 ( &),你可以克隆这个引用并传递它,但不能从中创建一个可变&mut引用。
  • 如果存在对某个对象的可变 ( &mut) 引用,则不能存在对该对象的其他引用。
  • 引用不允许比它所引用的对象寿命长,并且所有操作引用的函数都必须'a使用生命周期注释(如)声明来自其输入和输出的引用是如何链接的。

因此,就表达性而言,我们实际上比使用普通原始指针时受到更多限制(例如,仅使用安全引用无法构建图形结构),但这些规则可以在编译时有效地完全检查。

然而,仍然可以使用原始指针,但是您必须将处理它们的代码包含在一个unsafe { /* ... */ }块中,告诉编译器“相信我,我知道我在这里做什么”。这就是一些特殊的智能指针在内部所做的,例如RefCell,它允许您在运行时而不是编译时检查这些规则,以获得表现力。

于 2015-04-14T13:48:05.053 回答