在范围内拥有不正确(大于正确)生命周期的引用是否可以?

Luk*_*odt 6 unsafe lifetime rust

&'a T如果'a引用值大于引用值,引用是否会立即导致 UB(未定义行为)?或者,只要不超过 type 的引用值,就可以拥有这样的引用T吗?

作为比较:mem::transmute::<u8, bool>(2)是直接 UB,即使您从未访问过返回值。如果您有一个带有 value 的引用,情况也是如此0,因为引用必须始终有效。即使您从未访问过它们。另一方面,在ptr::null()您尝试取消引用空指针之前,拥有不是问题。

考虑这个代码:

let x = '';
let r_correct: &char = &x;

{
    let r_incorrect: &'static char = unsafe { mem::transmute(r_correct) };
    println!("{}", r_incorrect);
}
Run Code Online (Sandbox Code Playgroud)

在这段代码中,有两个对x. 都活不下去了x。但这种类型r_incorrect显然是谎言,因为x不会永远活着。

这段代码是否表现出明确定义的行为?我看到三个选项:

  • (a) 此代码表现出未定义的行为。
  • (b) 此代码的行为定义明确(“安全”)。
  • (c) Rust 还没有定义关于这部分语言的规则。

Pet*_*all 5

不。只有r_incorrectx超出范围后访问时才会发生未定义的行为,而您在这里没有这样做。

Rust 中的生命周期注释由编译器检查,以确保您没有做任何会导致内存不安全的事情,但是——假设借用检查器很高兴——它们对生成的二进制文件或变量实际存在的时间没有影响。

在您的示例中,您向编译器声称 的生命周期r_incorrect比实际长得多,但没有问题,因为您只能在其有效生命周期内访问它。

这样做的危险在于,未来对代码的更改可能会尝试r_incorrect在其真实生命周期之外使用。编译器无法阻止这种情况发生,因为您已经坚持认为没问题。


sk_*_*ant 2

据我所知,没有官方资源明确说明拥有和/或取消引用具有大于正确生命周期的引用是否会导致未定义的行为。然而,有多个资源讨论了 Rust 中的未定义行为以及具有无限生命周期的取消引用引用,这些资源给出了有关执行此操作的定义性的提示。

\n

根据 Rustonomicon 的说法,导致未定义行为的因素

\n

引自Rustonomicon,“不安全可以做什么”一章粗体突出显示[括号中的斜体文本]以及以下所有引文均由我撰写):

\n
\n

与 C 不同,Rust 中未定义行为的范围相当有限。所有核心语言关心的是防止以下事情:

\n
    \n
  • 取消引用(使用*运算符 on)悬空或未对齐的指针(见下文)
  • \n
  • 打破指针别名规则
  • \n
  • 使用错误的调用 ABI 调用函数或从具有错误的展开 ABI 的函数展开。
  • \n
  • 引发数据竞争
  • \n
  • 执行使用当前执行线程\n不支持的目标功能编译的代码
  • \n
  • 生成无效值(单独或作为复合类型的字段,例如\nas enum/ struct/数组/元组):\n
      \n
    • [许多与参考生命周期无关的子项]
    • \n
    \n
  • \n
\n

每当分配值、传递给函数/基元操作或从函数/基元操作返回值时,都会“生成”值。

\n

如果引用/指针为空或者它指向的所有字节并非都属于同一分配(因此特别是它们都必须是某个分配的一部分),则该引用/指针是“悬空”的。它指向的字节范围由指针值和指针类型的大小决定。因此,如果跨度为空,则“悬空”与“非空”相同。请注意,切片和字符串指向其整个范围,因此长度元数据永远不要太大(特别是分配,因此切片和字符串不能大于字节isize::MAX),这一点很重要。如果由于某种原因这太麻烦,\n请考虑使用原始指针。

\n

就是这样。这就是 Rust 中产生未定义行为的所有原因。当然,不安全的函数和特征可以自由声明程序必须维护的任意其他约束以避免未定义的行为。

\n
\n

只有前两个要点与指针和/或引用相关。

\n
    \n
  • 第一点是关于取消引用悬空指针和未对齐指针。

    \n
      \n
    • 未对齐的指针:改变引用的生命周期不能改变其对齐方式,因此这在这里不是问题。
    • \n
    • 悬空指针:在正确的生命周期内,根据上面的定义,引用不能悬空。因此,在正确的生命周期内,转变的引用也不是悬空的,并且可以取消引用而不会导致未定义的行为。
    • \n
    \n
  • \n
  • 第二点是关于Rust的指针别名规则。引用Rustonomicon 的“参考文献”一章(在讨论指针别名规则时,上面的引用中也链接了该章节):

    \n
    \n

    参考有两种:

    \n
      \n
    • 共享参考:&
    • \n
    • 可变参考:&mut
    • \n
    \n

    其遵循以下规则:

    \n
      \n
    • 引用不能比其引用对象更长久
    • \n
    • 可变引用不能使用别名
    • \n
    \n

    就是这样。这就是下面的整个模型参考。

    \n

    当然,我们应该定义别名的含义。

    \n
    error[E0425]: cannot find value `aliased` in this scope\n --> <rust.rs>:2:20\n  |\n2 |     println!("{}", aliased);\n  |                    ^^^^^^^ not found in this scope\n\nerror: aborting due to previous error\n
    Run Code Online (Sandbox Code Playgroud)\n

    不幸的是,Rust 还没有真正定义它的别名模型。

    \n

    当我们等待 Rust 开发人员指定其语言的语义时,让我们使用下一节来讨论一般情况下别名是什么,以及为什么它很重要。

    \n
    \n
      \n
    • 对于我们的情况 \xe2\x80\x93 “引用不能比其所指对象寿命更长”,第一点听起来确实不太好。然而,引用Rustonomicon,“生命周期”一章,“生命周期覆盖的区域”部分

      \n
      \n

      生命周期(有时称为借用)从创建到最后使用都是有效的借来的东西需要比活着的借物更长久

      \n
      \n

      因此,由于我们在正确的生命周期结束后没有使用该引用,因此它不再存在。因此,指示对象也不需要在正确的生命周期结束后还活着。

      \n
    • \n
    • 第二点仅与示例中的可变引用 \xe2\x80\x93 相关,使用共享引用。无论哪种方式,只要在转换的引用存在时不使用原始值,任何定义都不会发生指针别名(尽管,正如 Rustonomicon 所说,没有为 Rust 定义别名模型,所以语言律师关于这个很难......)

      \n
    • \n
    \n
  • \n
\n

结论 \xe2\x80\x93 根据 Rustonomicon 导致未定义行为的事情

\n

Rustonomicon 中触发未定义行为的事物的枚举不包含拥有和取消引用具有大于正确生命周期的引用,只要在正确生命周期结束后未访问该引用即可

\n

根据 Rust 参考,导致未定义行为的事情

\n

Rustonomicon 并不是唯一讨论未定义行为的文档。引用Rust 参考,“行为被视为未定义”一章

\n
\n

如果 Rust 代码表现出以下列表中的任何行为,则该代码不正确。\n unsafe这包括块和unsafe函数中的代码。\nunsafe仅意味着程序员需要避免未定义的行为;它不会改变 Rust 程序绝不能导致未定义行为这一事实。

\n

程序员在编写代码时有责任unsafe确保与代码交互的任何安全代码unsafe都不会触发这些行为。unsafe对于任何安全客户端满足此属性的代码\n称为sound;如果unsafe安全代码可以滥用代码来表现\n未定义的行为,那么它是不健全的

\n
\n

\xe2\x9a\xa0\xef\xb8\x8f警告:以下列表并不详尽。对于不安全代码中允许什么和不允许什么,Rust 的语义没有正式的模型,因此可能有更多的行为被认为是不安全的。以下列表正是我们所知道的\n肯定是未定义的行为。请阅读Rustonomicon。\n

\n
\n
    \n
  • 数据竞赛。
  • \n
  • 在悬空或未对齐的原始指针上计算取消引用表达式( ) ,即使在就地表达式上下文中(例如)。*expraddr_of!(&*expr)
  • \n
  • 违反指针别名规则&mut T&T遵循 LLVM\xe2\x80\x99s 作用域\n noalias模型,除非&T包含UnsafeCell<U>.
  • \n
  • 改变不可变数据。项目内的所有数据const都是不可变的。此外,通过共享引用到达的所有数据或不可变绑定拥有的数据都是不可变的,除非该数据包含在UnsafeCell<U>.
  • \n
  • 通过编译器内在函数调用未定义的行为。
  • \n
  • 执行使用当前平台不支持的平台功能编译的代码(请参阅target_feature)。
  • \n
  • 使用错误的调用 ABI 调用函数或从具有错误的展开 ABI 的函数展开。
  • \n
  • 即使在私人领域和当地人中也会产生无效值。每当将值分配给某个位置或从某个位置读取值、传递给函数/基元操作或从函数/基元操作返回时,都会“生成”值。\n以下值无效(在其各自的类型中) :\n
      \n
    • [许多与参考生命周期无关的子项]
    • \n
    \n
  • \n
\n
\n

注意:未初始化的内存对于任何具有有限有效值集的类型来说也是隐式无效的。换句话说,唯一允许读取未初始化内存的情况是unions 内部和“填充”(类型的字段/元素之间的间隙)。

\n
\n

笔记:未定义的行为会影响整个程序。例如,在 C 中调用表现出 C 未定义行为的函数意味着整个程序包含也可能影响 Rust 代码的未定义行为。反之亦然,Rust 中未定义的行为可能会对任何 FFI 调用其他语言执行的代码造成不利影响。

\n

悬空指针

\n

如果引用/指针为空或者它指向的所有字节并非都属于同一分配(因此特别是它们都必须是某个分配的一部分),则该引用/指针是“悬空”的。它指向的字节范围由指针值和被指针类型的大小(使用size_of_val)确定。因此,如果跨度为空,“悬挂”与“非空”相同。请注意,切片和字符串指向其整个范围,因此元数据的长度永远不要太大,这一点很重要。特别是,分配以及切片和字符串\n不能大于isize::MAX字节。

\n
\n

这个列表,包括粗体的两个点,或多或少相当于 Rustonomicon 中的列表(尽管不太严格,因为第一个粗体项目符号点仅禁止取消引用悬空原始指针,而不是取消引用悬空引用 \xe2\x80\ x93 我想这是一个疏忽)。LLVM 文档有一些有趣的链接,但最终结果是相同的:根据此列表,只要引用不存在,拥有大于正确生命周期的引用就不会导致未定义的行为。在正确的生命周期结束后取消引用。不过,这里还有一个附加说明:

\n
\n

\xe2\x9a\xa0\xef\xb8\x8f警告:以下列表并不详尽。对于不安全代码中允许什么和不允许什么,Rust 的语义没有正式的模型,因此可能有更多的行为被认为是不安全的。以下列表正是我们所知道的\n肯定是未定义的行为。请在编写不安全代码之前阅读 [Rustonomicon]。

\n
\n

结论 \xe2\x80\x93 根据 Rust 参考,导致未定义行为的事情

\n

Rust 参考并未包含所有可能触发未定义行为的详尽枚举。虽然 Rust 参考文献没有明确指出生命周期大于正确生命周期的引用确实触发未定义的行为,但它也没有明确指出它们不会

\n

Rustonomicon 的无限生命周期

\n

引自Rustonomicon,“无限生命”一章

\n
\n

不安全的代码通常最终会凭空产生引用或生命周期。\n这样的生命周期以无界的形式出现。最常见的来源是取消引用原始指针,这会产生具有无限生命周期的引用。\n这样的生命周期会根据上下文的需要而变得尽可能大。这实际上比简单地变成 更强大\'static,因为例如&\'static &\'a T将无法进行类型检查,但未绑定的生命周期将&\'a &\'a T根据需要完美地塑造成。然而,对于大多数意图和目的来说,这样的无界\n生命周期可以被视为\'static

\n

几乎没有参考\'static,所以这可能是错误的。transmute和 \ntransmute_copy是另外两个主犯。人们应该努力尽快限制无限的生命周期,尤其是跨函数边界。

\n
\n

Rustonomicon 表示,人们应该“努力尽快限制无限的生命周期,尤其是跨越功能边界”。它没有表明取消引用无界引用 \xe2\x80\x93 假设引用对象仍然存在 \xe2\x80\x93 将导致未定义的行为。由于解除引用是一种常见的操作,我无法想象 Rustonomicon 没有指出如此明显的陷阱。因此,我得出的结论是,只要引用对象仍然存在,取消引用无界引用就不会导致未定义的行为。

\n

然而,问题不是关于具有无限生命周期的引用,而是关于具有大于正确生命周期的引用,例如&\'static T. Rustonomicon 指出“对于大多数意图和目的来说,[...]无限的生命周期可以被视为\'static”。这并不一定意味着取消引用具有大于正确生存期的引用与取消引用具有无界生存期的引用一样是已定义的行为。但是,我不明白为什么rustc在这方面应该以不同的方式处理无限的生命周期。如果确实如此,我希望 Rustonomicon 会包含一条注释,表明确实如此,并且无限制的生命周期仍然比错误限制的生命周期更安全。

\n

结论 \xe2\x80\x93 Rustonomicon 的无限生命周期

\n

根据 Rustonomicon,取消引用无限生命周期可能不是未定义的行为。这可能会或可能不会扩展到有界的引用,但在我看来,大于正确的生命周期 \xe2\x80\x93 ,它确实会扩展。

\n

transmute()文档示例

\n

引用标准库文档std::mem::transmute(),第二个示例:

\n
\n

延长寿命[...]。这是高级的、非常不安全的 Rust!

\n
struct R<\'a>(&\'a i32);\nunsafe fn extend_lifetime<\'b>(r: R<\'b>) -> R<\'static> {\n    std::mem::transmute::<R<\'b>, R<\'static>>(r)\n}\n\n*[...]*\n
Run Code Online (Sandbox Code Playgroud)\n
\n

这是您将得到的明确证据,至少具有大于正确生命周期的引用不会导致未定义的行为 \xe2\x80\x93 否则,任何使用任何非引用调用此函数的人\'static都会立即调用未定义的行为,并且这个功能将非常非常无用。此外,对我来说,这意味着您还可以取消引用extend_lifetime\xe2\x80\x93 返回的引用,否则该函数的好处是什么?

\n

结论 \xe2\x80\x93 示例transmute()示例

\n

上的例子transmute()似乎暗示取消引用具有大于正确生命周期的引用是明确定义的。

\n

定论

\n

遗憾的是,有关 Unsafe Rust 详细信息的文档仍然不完整,并且存在许多关于像这样的边缘情况的问题,文档根本无法给出明确的答案。然而,所有与该问题远程相关的文档似乎都暗示取消引用有问题的引用实际上是明确定义的行为。这是否足以让你做这样的事情取决于你 \xe2\x80\x93 它可能适合我。

\n

但是,你真的不应该这样做

\n

澄清一下:虽然这段代码可能定义良好,但它绝对仍然是一把枪。跨函数边界传递这样的引用是一个坏主意,特别是当该函数可以pub被其他模块/包调用时。即使不跨函数边界传递它,仍然很容易误用此引用,从而导致代码导致未定义的行为。如果您认为可能需要这样做,我强烈建议您重新考虑是否可以重构代码以避免改变生命周期。例如,直接使用原始指针而不是生命周期不正确的引用可能更安全(或者至少更清楚这是完全不安全的)。

\n