可变访问器使用强制转换是安全的吗?

7

我正在尝试理解get函数中的&&mut重复代码问题。我试图理解是否使用unsafe块内的强制类型转换来解决这个问题是安全的。

以下是问题的示例,取自非常好的教程使用过多的链接列表学习Rust

type Link<T> = Option<Box<Node<T>>>;

pub struct List<T> {
    head: Link<T>,
}

struct Node<T> {
    elem: T,
    next: Link<T>,
}

impl<T> List<T> {
    // Other methods left out...

    // The implementation of peek is simple, but still long enough
    // that you'd like to avoid duplicating it if that is possible.
    // Some other getter-type functions could be much more complex
    // so that you'd want to avoid duplication even more.
    pub fn peek(&self) -> Option<&T> {
        self.head.as_ref().map(|node| {
            &node.elem
        })
    }

    // Exact duplicate of `peek`, except for the types
    pub fn peek_mut(&mut self) -> Option<&mut T> {
        self.head.as_mut().map(|node| {
            &mut node.elem
        })
    }
}

解决方案

在一个unsafe块中使用转换似乎可以解决这个问题。该解决方案具有以下特性:

  • 它可以以安全的方式完成。
  • 它为不安全的实现提供了安全接口。
  • 实现简单。
  • 它消除了代码重复。

以下是解决方案:

// Implemention of peek_mut by casting return value of `peek()`
pub fn peek_mut(&mut self) -> Option<&mut T> {
    unsafe {
        std::mem::transmute(self.peek())
    }
}

这是我认为它看起来安全的理由:
  1. peek()的返回值是从已知源具有已知别名情况的位置获取的。
  2. 由于参数类型是&mut self,因此不存在对其元素的引用。
  3. 因此,peek()的返回值未被赋予别名。
  4. peek()的返回值不会逃逸出该函数体。
  5. 将未赋予别名的&强制转换为&mut似乎不违反指针别名规则。
  6. 转换的目标和源的生命周期相匹配。

其他说明


问题

我有以下问题想要询问比我更了解Rust的人士:

  • peek_mut提供的实现是否安全?
  • 是否还需要其他必要的参数来确定其是否安全,我已经漏掉了吗?
  • 如果确实不安全,那么原因是什么?你能详细解释一下吗?
  • 如果是这样,是否有类似的解决方案是安全的?

3
无论价值如何,即使这被证明是安全的,我也不会这样做。它以一点重复为代价,换来了你在阅读代码时需要思考的内容,以及使相关更改或审计不安全时需要考虑的不安全性。 - Ry-
@Ry-:这是一个很好的观点...但如果getter只是稍微复杂一些,而且有很多个,那么维护重复版本就会变得非常痛苦...无论如何,了解它是否安全还是很有趣的! - Lii
1
例如,您可以创建一个私有的peek_raw函数,该函数返回原始指针,并使用它来编写peekpeek_mut - 我认为这是一种避免从&T转换为&mut T不安全性的方法,因为原始指针可能与任何东西别名。但是,它生成的代码可能不如简单地两次编写“相同”的(或几乎相同的)代码好。 - trent
@trentcl:啊,那听起来是个不错的替代方案,谢谢!除了性能之外,该解决方案的另一个缺点是,基于&peek实现将比peek_raw更容易工作,因为它不会有正常的Rust安全检查。 - Lii
确实,安全性将由peekpeek_mut执行,但在我看来,这与您建议使用transmute基本相同。 - trent
我鼓励你阅读一些Rust koans。我认为"Obstacles"和"Puom"都与这个问题相关。 - trent
1个回答

7
我认为这段代码将会引发未定义行为。引用 Nomicon 的话:

  • 转换 & 为 &mut 是未定义行为
    • 将 & 转换成 &mut 总是会引起未定义行为
    • 不可以这样做
    • 你没有特权

更重要的是,Rust编译器会在LLVM中间表示中将peek()的返回值标记为不可变的,而LLVM可以根据这个声明进行优化。虽然在这种特定情况下可能目前不会出现,但我仍然认为这是未定义行为。如果您想以任何代价避免重复,可以使用宏。


1
是的,我在Nomicon中看到了那个部分。让我感到困扰的是它没有解释更多!你能提供任何关于&返回值被标记为“不可变”的事实来源吗?而且,在所有情况下都强制转换是UD的吗?Rust或LLVM参考文档中有关于这方面的内容吗? - Lii
1
@Lii:将&转化为&mut 始终 是UB(未定义行为)的说法是“在所有情况下”一个相当好的证明,不是吗? - Ry-
我想了解更多,以便理解为什么如果是这样的话它就是UB。你通过说“编译器将标记peek的返回值为不可变”暗示了一个解释,我想找到更多关于这方面的细节。你知道为什么会出现UD的更多细节吗?或者你知道在哪里可以找到更多这样的细节吗? - Lii
Rust参考文献并不是那么明确,它说“违反指针别名规则”会导致UB,并链接到LLVM参考文献。也许可以从LLVM别名规则中推断出这是UB?但要做到这一点,似乎需要比我所拥有的更深入的知识。 - Lii
5
话虽如此,“因为Rust这么说”所以它是未定义的行为。它不是由于编译器必须执行某些魔法而导致UB,而是允许编译器基于所有良好形式的程序没有UB的假设来进行优化。UB的分类非常广泛,因为它限制了良好形式程序的数量,并使编译器更容易进行优化,但并不一定意味着每种未定义行为都有一些编译器优化背后。它只是表示编译器可以自由地这样做。 - Frxstrem
显示剩余5条评论

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