Как избежать ненужных проверок соответствия или использования небезопасных при перемещении значения из структуры, которая реализует Drop? - PullRequest
1 голос
/ 25 февраля 2020

У меня есть структура B, которая реализует черту Trait методом do_something. Мне нужно выполнить некоторые дополнительные действия, когда struct B отброшена, если эта функция не была вызвана. В частности, если do_something никогда не вызывался, Vec<A> должен быть заполнен A::None:

enum A {
    V1,
    V2,
    None,
}

struct B {
    data: Option<(A, Vec<A>)>,
}

trait Trait {
    fn do_something(self) -> Vec<A>;
}

impl Trait for B {
    fn do_something(mut self) -> Vec<A> {
        let (a, mut vec) = self.data.take().unwrap();
        vec.push(a);
        vec
    }
}

impl Drop for B {
    fn drop(&mut self) {
        match self.data.take() {
            Some((a, mut vec)) => vec.push(A::None),
            _ => {}
        }
    }
}

Это имеет некоторые логически ненужные проверки match. Я хочу избежать их и предложил следующее решение:

struct B {
    data: (A, Vec<A>),
}

trait Trait {
    fn do_something(self) -> Vec<A>;
}

impl Trait for B {
    fn do_something(mut self) -> Vec<A> {
        let (a, mut vec) = std::mem::replace(&mut self.data, unsafe {
            std::mem::MaybeUninit::<(A, Vec<A>)>::uninit().assume_init()
        });
        std::mem::forget(self);
        vec.push(a);
        vec
    }
}

impl Drop for B {
    fn drop(&mut self) {
        self.data.1.push(A::None)
    }
}
  1. Правильно ли мое решение unsafe? Содержит ли оно неопределенное поведение?
  2. Можно ли избежать использования либо unsafe, либо обтекания B.data в Option для достижения вышеуказанного поведения?

1 Ответ

0 голосов
/ 25 февраля 2020

Это не только перемещает значения из структуры, но также записывает неинициализированную память в структуру. Компилятор предупреждает, что это приведет к неопределенному поведению:

warning: the type `(A, std::vec::Vec<A>)` does not permit being left uninitialized
  --> src/main.rs:21:22
   |
21 |             unsafe { std::mem::MaybeUninit::<(A, Vec<A>)>::uninit().assume_init() },
   |                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |                      |
   |                      this code causes undefined behavior when executed
   |                      help: use `MaybeUninit<T>` instead, and only call `assume_init` after initialization is done
   |
   = note: `#[warn(invalid_value)]` on by default
note: std::ptr::Unique<A> must be non-null (in this struct field)

Лучшее решение - использовать mem::transmute():

#[repr(transparent)]
struct B {
    data: (A, Vec<A>),
}

impl Trait for B {
    fn do_something(self) -> Vec<A> {
        // Safety: Since `B` is transparent, `B` and (A, Vec<A>) have the same size and layout.
        let (a, mut vec): (A, Vec<A>) = unsafe { std::mem::transmute(self) };
        vec.push(a);
        vec
    }
}

Обратите внимание, что это предотвращает вызов Drop. Если реализация Drop освобождает память или другие ресурсы, вы должны сделать это вручную.

...