#rust #reference #undefined-behavior #unsafe
#Ржавчина #ссылка #неопределенное поведение #небезопасно
Вопрос:
Недавно я написал следующее:
use std::ptr;
fn modify_mut_ret<T,R,F> (ptr: amp;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: amp;mut T, f: F) -> R
where F: FnOnce(T) -> (T,R),
T: Default
{
let mut t = T::default();
mem::swap(ptr, amp;mut t);
let (t,r) = f(t);
*ptr = t;
r
}
Я не думаю, что первая реализация содержит какое-либо неопределенное поведение: у нас нет проблем с выравниванием, и мы, с ptr::write
помощью, устраняем одно из двух дублированных прав собственности ptr::read
. Однако меня беспокоит тот факт, что std
, по-видимому, не содержит функции с таким поведением. Я что-то не так понял или я что-то забыл? Содержит ли приведенный выше небезопасный код какой-либо UB?
Комментарии:
1. На самом деле,
r
для возвращаемого значения. В конечном счете в этом нет необходимости, поскольку мы можем подготовитьlet mut r:R = R::default();
и позволитьf: impl FnOnce(T) -> T
заменить значениеr
. Однако это небольшая проблема, и я хотел ее избежать. Упс, ты ушел? (Предыдущий комментарий исчез)2. Да, я понял, что это, вероятно, для возвращаемого значения, поэтому я удалил свой комментарий.
3. Если
f
паникует,ptr
значение ’s сбрасывается дважды. Я не думаю, что эта функция может существовать.4.Владение возвращаемым значением, документированное на,
std::ptr::read
похоже, указывает на то, что это нормально, хотя в нем ничего не говорится о панике, и у меня те же проблемы, что и у Ry. Пользователю очень сложно гарантировать, что «Закрытие не должно вызывать паники».5. Это очень похоже на
replace_with
ящик. В документах есть ссылка на RFC, который пытался включить его в std, но потерпел неудачу.
Ответ №1:
Этот код содержит только один экземпляр UB, что связано с тем, что функция может вернуться раньше. Давайте рассмотрим это поближе (я переместил некоторые вещи, чтобы их было легче разобрать):
fn modify_mut_ret<T, R, F: FnOnce(T) -> (T, R)>(x: amp;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<...>>
или глобальной переменной.
Однако, если он запаникует, и паника будет поймана кодом, вызывающим modify_mut_ret
using std::panic::catch_unwind
, будет две копии старого значения x
, потому ptr::write
что оно еще не было достигнуто, но ptr::read
уже было.
Что вам нужно сделать, так это справиться с паникой, прервав процесс:
use std::{ptr, panic::{catch_unwind, AssertUnwindSafe}};
fn modify_mut_ret<T, R, F>(x: amp;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
документации.