将类型转换为`*mut`会覆盖引用不是`mut`的限制。

5

我正在将结构体的字段转换为*mut指针,因此我将该结构体的引用声明为可变的。但是,我开始收到警告,说明mut修饰符不是必需的。这对我来说很奇怪,我显然正在进行修改操作,但声明为mut是不必要的?这导致我想到了这个最小示例:

struct NonMut {
    bytes: [u8; 5]
}

impl NonMut {
    pub fn get_bytes(&self) -> &[u8] {
        &self.bytes
    }
}

fn potentially_mutate(ptr: *mut u8) {
    unsafe { *ptr = 0 }
}

fn why(x: &NonMut) {
    let ptr = x.get_bytes().as_ptr() as *mut u8;
    
    potentially_mutate(ptr);

}

fn main() {
    let x = NonMut { bytes: [1, 2, 3, 4, 5] };
    println!("{}", x.get_bytes()[0]);

    why(&x);
    
    println!("{}", x.get_bytes()[0]);
}

1
0

代码示例链接

显然,解引用指针需要使用不安全代码,但对于刚开始编写自己的应用程序并决定调用why(可能在一个外部crate中定义) 的人来说,这种行为似乎完全出乎意料。你传递了一个明显是不可变的引用,借贷检查器标记你的代码正确,但在底层,为该结构创建了一个可变引用。这可能会导致多个生存的可变引用被创建到NonMut,而why的用户却无法知道。

这种行为是否符合预期?难道将*mut强制转换为操作数需要首先是mut吗?没有必要这样做的充分理由吗?


我没有收到任何警告。 - Stargateur
@Stargateur 如果您有一个类型为NonMut的本地mut变量,并使用它来获取mut*并通过该变量进行突变,则会出现警告。 - V0ldek
2个回答

12
问题#1:当传入无效指针时,potentially_mutate可能会导致未定义行为,但它被错误地标记为安全函数。因此,我们需要将其标记为unsafe并记录其安全使用的假设:

potentially_mutate被错误标记为安全函数,实际上会导致未定义行为(例如,如果我传入一个无效指针会怎样?)。因此,我们需要将其标记为unsafe,并记录其安全使用的假设:

/// Safety: must pass a valid mutable pointer.
unsafe fn potentially_mutate(ptr: *mut u8) {
    unsafe { *ptr = 0 }
}

所以现在我们需要重写 why

fn why(x: &NonMut) {
    let ptr = x.get_bytes().as_ptr() as *mut u8;
    
    unsafe {
        potentially_mutate(ptr);
    }
}

现在我们暴露了问题 #2: 我们违反了 potentially_mutate 所做的假设。我们传递了一个指向不可变数据的指针,声称它是可变的。这是未定义的行为!即使我们有 x: &mut NonMut,也不会有任何区别,这就是如果你尝试的话会得到警告的原因。重要的是:

pub fn get_bytes(&self) -> &[u8] {
    &self.bytes
}

当您调用方法时,我们构造一个不可变的字节片段的不可变引用,而不管您的x:&NonMut是否可变。如果我们想要改变字节,我们还需要进行以下操作:

pub fn get_bytes_mut(&mut self) -> &mut [u8] {
    &mut self.bytes
}

然而,您还没有完成,因为as_ptr指出:

调用者还必须确保指针(非直接转移)指向的内存从未使用此指针或从此指针派生的任何指针进行写入(除了在UnsafeCell中)。如果需要改变切片的内容,请使用as_mut_ptr。

as_mut_ptr也有相应说明:

调用者必须确保切片的生命周期超过此函数返回的指针,否则它将指向垃圾。

因此,要正确且安全地完成您的示例:

struct Mut {
    bytes: [u8; 5]
}

impl Mut {
    pub fn get_bytes(&self) -> &[u8] {
        &self.bytes
    }
    
    pub fn get_bytes_mut(&mut self) -> &mut [u8] {
        &mut self.bytes
    }
}

unsafe fn potentially_mutate(ptr: *mut u8) {
    *ptr = 0
}

fn whynot(x: &mut Mut) {
    unsafe {
        let slice = x.get_bytes_mut(); // Keep slice alive.
        let ptr = slice.as_mut_ptr();
        potentially_mutate(ptr);
    }
}

fn main() {
    let mut x = Mut { bytes: [1, 2, 3, 4, 5] };
    println!("{}", x.get_bytes()[0]);
    whynot(&mut x);
    println!("{}", x.get_bytes()[0]);
}

我同意如何改进这个问题的评论,但我的原始问题仍然存在——为什么我可以从一个非可变引用“&NonMut”获取的非可变引用“&[u8]”制作一个*mut u8?在我看来,借用检查器不应该允许这种转换。 - V0ldek
4
因为构建这样的指针并不会导致未定义行为,只有在错误使用它们时才会出现问题。而对于引用,即使构造它们也将导致未定义行为。 - orlp
1
@V0ldek 因为你可以,例如,执行 &mut v as *mut T as *const T as *mut T(第一个 as *mut T 不应该是必需的,但目前由于编译器中的错误而需要)。或者您可以在多个语句中执行此操作,并且 _解引用该指针是有效的_。有一些用例(例如,std::ptr::NonNull 包含一个 *const T,但允许您获取 *mut T。唯一不允许这样做的时间是当您从共享 引用 派生指针时。 - Chayim Friedman
@orlp 关于将 potentially_mutate() 设为 unsafe 的观点可以有争议。由于该函数是私有的,只要没有暴露不安全的代码,保持其安全性以减少噪音可能是可行的。这完全取决于程序员的意见(我们确实努力追求最小的安全封装,而函数是一个很好的候选对象,但有时您只能使整个模块或甚至整个 crate 安全)。 - Chayim Friedman
@orlp 理解了:它需要 切片([T], 无大小) 存在于内存中,而不是 对它的引用 (&{mut }[T])。它不能要求基础存储器存在于内存中 - 因为例如,调整Vec的大小并不会使其失效,但会使任何对它曾经包含的切片的引用无效。所以,不需要保持变量 slice 存活。 - Chayim Friedman
显示剩余6条评论

0
你不能直接从&u8转换为*mut u8
fn allowed(x: &[u8]) {
    let x_ptr   = x.as_ptr();    // &[u8]     -> *const u8
    let _ptr = x_ptr as *mut u8; // *const u8 -> *mut u8 
}

游乐场

*const T转换为*mut T是允许的。

如果您想在可变性上进行抽象,并且希望有一些运行时检查来告诉您指针是来自可变还是不可变源,则这可能很有用。

但是,直接将不可变引用转换为可变指针是不允许的,因此以下示例无法按预期编译:

fn not_allowed(x: &[u8]) {
    let x_ref  = &x[0];          // &[u8] -> &u8
    let _ptr = x_ref as *mut u8; // &u8   -> *mut u8
}

游乐场

请注意,虽然第一个示例是允许的,并且由于我们没有使用可变原始指针,甚至不应该有未定义的行为。 对可变原始指针进行解引用或将其转换为可变引用将导致未定义的行为。因为原始指针是从共享引用派生出来的,并且没有涉及UnsafeCell,正如其他人所指出的那样。


1
这是完全错误的。从共享引用创建可变引用是未定义行为,无论中间有什么。@orlp已经正确回答了。 - Chayim Friedman
我并不是想说这不是未定义行为。第一个片段只是尝试以更小的步骤解释问题中到底发生了什么,而第二个片段甚至无法编译。 - Skgland
将函数名称从allowed更改为allowed_but_ub,以使其更清晰 @ChayimFriedman - Skgland

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