Rust:用函数修改引用的引用;这个有UB吗?
Rust: Modifying the referent of a reference with a function; does this contain UB?
最近,我写了以下内容:
use std::ptr;
fn modify_mut_ret<T,R,F> (ptr: &mut T, f: F) -> R
where F: FnOnce(T) -> (T,R)
{
unsafe {
let (t,r) = f(ptr::read(ptr));
ptr::write(ptr,t);
r
}
}
这是一个简单的实用程序,所以我希望它在标准库中,但我找不到它(至少在 std::mem
中)。例如,如果我们假设 T: Default
,我们可以通过额外的 drop
开销安全地实现它:
use std::mem;
#[inline]
fn modify_mut_ret<T,R,F>(ptr: &mut T, f: F) -> R
where F: FnOnce(T) -> (T,R),
T: Default
{
let mut t = T::default();
mem::swap(ptr, &mut t);
let (t,r) = f(t);
*ptr = t;
r
}
我认为第一个实现不包含任何未定义的行为:我们没有对齐问题,并且我们使用 ptr::write
消除了使用 ptr::read
重复的两个所有权之一。但是,我很担心 std
似乎不包含具有这种行为的函数。我有什么不对或我忘记了什么吗?上面的不安全代码是否包含任何UB?
此代码仅包含一个 UB 实例,这是因为该函数可以 return 提早。让我们仔细看看它(我移动了一些东西以便更容易拆开):
fn modify_mut_ret<T, R, F: FnOnce(T) -> (T, R)>(x: &mut T, f: F) -> R {
unsafe {
let old_val = ptr::read(x); // Copied from original value, two copies of the
// same non-Copy object exist now
let (t, r) = f(old_val); // Supplied one copy to the closure
ptr::write(x, t); // Erased the second copy by writing without dropping it
r
}
}
如果闭包运行良好,外部函数将正常进行,并且 x
的旧值的副本总数将保持为一个副本,该副本将由闭包拥有,它可能会或可能不会存储在 Rc<RefCell<...>>
/Arc<RwLock<...>>
或全局变量中以备后用。
但是,如果它发生恐慌,并且恐慌被使用 std::panic::catch_unwind
调用 modify_mut_ret
的代码捕捉到,就会有 两个 旧副本x
的值,因为 ptr::write
尚未达到,但 ptr::read
已经达到。
您需要做的是通过中止进程来处理恐慌:
use std::{ptr, panic::{catch_unwind, AssertUnwindSafe}};
fn modify_mut_ret<T, R, F>(x: &mut T, f: F) -> R
where F: FnOnce(T) -> (T, R) {
unsafe {
let old_val = ptr::read(x);
let (t, r) = catch_unwind(AssertUnwindSafe(|| f(old_val)))
.unwrap_or_else(|_| std::process::abort());
ptr::write(x, t); // Erased the second copy by writing without dropping it
r
}
}
这样,闭包中的恐慌将永远不会离开函数,因为它会在任何其他代码观察到重复值之前捕获恐慌并立即中止进程。
AssertUnwindSafe
在那里是因为我们必须确保我们不会观察到由于恐慌而创建的逻辑上无效的值,因为我们总是会在恐慌之后中止。有关详细信息,请参阅 UnwindSafe
's documentation。
最近,我写了以下内容:
use std::ptr;
fn modify_mut_ret<T,R,F> (ptr: &mut T, f: F) -> R
where F: FnOnce(T) -> (T,R)
{
unsafe {
let (t,r) = f(ptr::read(ptr));
ptr::write(ptr,t);
r
}
}
这是一个简单的实用程序,所以我希望它在标准库中,但我找不到它(至少在 std::mem
中)。例如,如果我们假设 T: Default
,我们可以通过额外的 drop
开销安全地实现它:
use std::mem;
#[inline]
fn modify_mut_ret<T,R,F>(ptr: &mut T, f: F) -> R
where F: FnOnce(T) -> (T,R),
T: Default
{
let mut t = T::default();
mem::swap(ptr, &mut t);
let (t,r) = f(t);
*ptr = t;
r
}
我认为第一个实现不包含任何未定义的行为:我们没有对齐问题,并且我们使用 ptr::write
消除了使用 ptr::read
重复的两个所有权之一。但是,我很担心 std
似乎不包含具有这种行为的函数。我有什么不对或我忘记了什么吗?上面的不安全代码是否包含任何UB?
此代码仅包含一个 UB 实例,这是因为该函数可以 return 提早。让我们仔细看看它(我移动了一些东西以便更容易拆开):
fn modify_mut_ret<T, R, F: FnOnce(T) -> (T, R)>(x: &mut T, f: F) -> R {
unsafe {
let old_val = ptr::read(x); // Copied from original value, two copies of the
// same non-Copy object exist now
let (t, r) = f(old_val); // Supplied one copy to the closure
ptr::write(x, t); // Erased the second copy by writing without dropping it
r
}
}
如果闭包运行良好,外部函数将正常进行,并且 x
的旧值的副本总数将保持为一个副本,该副本将由闭包拥有,它可能会或可能不会存储在 Rc<RefCell<...>>
/Arc<RwLock<...>>
或全局变量中以备后用。
但是,如果它发生恐慌,并且恐慌被使用 std::panic::catch_unwind
调用 modify_mut_ret
的代码捕捉到,就会有 两个 旧副本x
的值,因为 ptr::write
尚未达到,但 ptr::read
已经达到。
您需要做的是通过中止进程来处理恐慌:
use std::{ptr, panic::{catch_unwind, AssertUnwindSafe}};
fn modify_mut_ret<T, R, F>(x: &mut T, f: F) -> R
where F: FnOnce(T) -> (T, R) {
unsafe {
let old_val = ptr::read(x);
let (t, r) = catch_unwind(AssertUnwindSafe(|| f(old_val)))
.unwrap_or_else(|_| std::process::abort());
ptr::write(x, t); // Erased the second copy by writing without dropping it
r
}
}
这样,闭包中的恐慌将永远不会离开函数,因为它会在任何其他代码观察到重复值之前捕获恐慌并立即中止进程。
AssertUnwindSafe
在那里是因为我们必须确保我们不会观察到由于恐慌而创建的逻辑上无效的值,因为我们总是会在恐慌之后中止。有关详细信息,请参阅 UnwindSafe
's documentation。