Rust: изменение референта ссылки с помощью функции; содержит ли это UB?

#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 документации.