Как разрушить объект, не уронив его?

У меня есть структура, которую я хочу взять по значению, изменить и затем вернуть. Я также хочу изменить его общий тип, поскольку я использую это состояние для статического обеспечения правильного порядка вызовов функций для создания безопасного FFI (детская площадка):

use core::marker::PhantomData;

struct State1 {}
struct State2 {}
struct Whatever {}

struct X<State> {
    a: Whatever,
    b: Whatever,
    c: Whatever,
    _d: PhantomData<State>,
}

impl<State> Drop for X<State> {
    fn drop(&mut self) {}
}

fn f(x: X<State1>) -> X<State2> {
    let X { a, b, c, _d } = x;
    //mutate a, b and c
    X {
        a,
        b,
        c,
        _d: PhantomData,
    } // return new instance
}

Поскольку X реализует Drop, я получаю:

error[E0509]: cannot move out of type `X<State1>`, which implements the `Drop` trait
  --> src/lib.rs:19:29
   |
19 |     let X { a, b, c, _d } = x;
   |             -  -  -         ^ cannot move out of here
   |             |  |  |
   |             |  |  ...and here
   |             |  ...and here
   |             data moved here
   |
   = note: move occurs because these variables have types that don't implement the `Copy` trait

Я не хочу ничего терять, так как я не уничтожаю x, а просто переупаковываю его. Как идиоматично предотвратить падение x?


person alagris    schedule 19.07.2021    source источник
comment
@alagris Я неправильно истолковал ваш первоначальный вопрос, и мой ответ упустил основную мысль того, о чем вы спрашивали. Правки Шепмастера делают его намного яснее.   -  person Peter Hall    schedule 19.07.2021
comment
Как оказалось, вторая часть моего первоначального ответа все еще работает в вашем случае, поэтому я восстановил ее.   -  person Peter Hall    schedule 19.07.2021


Ответы (4)


Вы можете отделить отслеживающую состояние PhantomData от удаляемой структуры:

use core::marker::PhantomData;

struct State1 {}
struct State2 {}
struct Whatever {}

struct Inner {
    a: Whatever,
    b: Whatever,
    c: Whatever,
}

struct X<State> {
    i: Inner,
    _d: PhantomData<State>,
}

impl Drop for Inner {
    fn drop(&mut self) {}
}

fn f(x: X<State1>) -> X<State2> {
    let X { i, _d } = x;
    //mutate i.a, i.b and i.c
    X {
        i,
        _d: PhantomData,
    } // return new instance
}

Это позволяет избежать небезопасности и гарантирует, что a, b и c будут храниться в группе и будут удалены вместе.

person Jmb    schedule 20.07.2021

Перемещение данных из значения оставит его в неопределенном состоянии. Это означает, что когда Drop::drop автоматически запускается компилятором, вы создаете неопределенное поведение.

Вместо этого мы можем использовать небезопасный Rust, чтобы предотвратить автоматическое удаление значения, а затем вытащить поля самостоятельно. Как только мы вытащим одно поле через ptr::read, исходная структура только частично инициализирован, поэтому я также использую MaybeUninit:

fn f(x: X<State1>) -> X<State2> {
    use std::{mem::MaybeUninit, ptr};

    // We are going to uninitialize the value.
    let x = MaybeUninit::new(x);

    // Deliberately shadow the value so we can't even try to drop it.
    let x = x.as_ptr();

    // SAFETY[TODO]: Explain why it's safe for us to ignore the destructor.
    // I copied this from Stack Overflow and didn't even change the comment!
    unsafe {
        let a = ptr::read(&(*x).a);
        let b = ptr::read(&(*x).b);

        X {
            a,
            b,
            _s: PhantomData,
        }
    }
}

Вы должны быть осторожны, чтобы получить все поля из x, иначе вы можете вызвать утечку памяти. Однако, поскольку вы создаете новую структуру, для которой нужны те же поля, в данном случае это маловероятно.

Смотрите также:

person Shepmaster    schedule 19.07.2021

Контракт, который вы создали с компилятором путем реализации Drop, заключается в том, что у вас есть код, который должен выполняться при уничтожении X, и что X должен быть завершен для этого. Деструктуризация противоречит этому контракту.

Вы можете использовать ManuallyDrop, чтобы избежать вызова Drop, но это не обязательно поможет вам разрушить его, вам все равно придется вытаскивать поля самостоятельно. Вы можете использовать std::mem::replace или std::mem::swap, чтобы переместить их, оставив на их месте фиктивные значения.

let mut x = ManuallyDrop::new(x);
let mut a = std::mem::replace(&mut x.a, Whatever {});
let mut b = std::mem::replace(&mut x.b, Whatever {});
let mut c = std::mem::replace(&mut x.c, Whatever {});

// mutate a, b, c

X { a, b, c, _d: PhantomData }

Примечание: это также предотвратит падение манекенов a, b и c; потенциально вызывая проблемы или утечку памяти в зависимости от Whatever. Так что я бы посоветовал не делать этого и использовать ответ Питера Холла, если unsafe неприятный.


Если вы действительно хотите такого же поведения и не хотите создавать фиктивные значения, вы можете использовать код unsafe через std::ptr::read, чтобы переместить значение с обещанием, что оригинал не будет доступен.

let x = ManuallyDrop::new(x);
let mut a = unsafe { std::ptr::read(&x.a) };
let mut b = unsafe { std::ptr::read(&x.b) };
let mut c = unsafe { std::ptr::read(&x.c) };
drop(x); // ensure x is no longer used beyond this point

// mutate a, b, c

X { a, b, c, _d: PhantomData }

Другим unsafe вариантом может быть использование std::mem::transmute для перехода напрямую из с X<State1> по X<State2>.

let mut x: X<State2> = unsafe { std::mem::transmute(x) };

// mutate x.a, x.b, x.c

x

Если тип состояния на самом деле вообще не используется для полей (это означает, что все X действительно идентичны), это вероятно безопасно, учитывая, что вы также декорируете X с помощью #[repr(C)], чтобы гарантировать, что компилятор не перемещает поля. вокруг. Но я могу упустить какую-то другую гарантию, std::mem::transmute очень unsafe и легко ошибиться.

person kmdreko    schedule 19.07.2021

Вы можете избежать кода unsafe, как это предлагается в других ответах, гарантируя, что каждое значение заменяется значением при его перемещении, чтобы x никогда не оставался в недопустимом состоянии.

Если типы полей реализуют Default, вы можете использовать std::mem::take:

use std::mem;

fn f(mut x: X<State1>) -> X<State2> {
    let mut a = mem::take(&mut x.a);
    let mut b = mem::take(&mut x.b);
    let mut c = mem::take(&mut x.c);
    
    // mutate a, b and c
    // ...

    // return a new X    
    X { a, b, c, _d: PhantomData }
}

Теперь можно безопасно удалить x, поскольку он содержит допустимые значения для каждого поля. Если типы полей не реализуют Default, вы можете вместо этого использовать std::mem::swap, чтобы заменить их подходящим фиктивным значением.

person Peter Hall    schedule 19.07.2021