Есть ли способ сказать компилятору Rust вызывать drop для частично инициализированных элементов массива при обработке паники? - PullRequest
0 голосов
/ 29 апреля 2018

Я работаю над пользовательским типом, где у меня есть следующие требования:

  1. Коллекция элементов, которые избегают выделения кучи. Я использую массивы вместо Vec.
  2. Коллекция содержит не копируемые типы
  3. Реализует Default для типов, которые также реализуют Default
  4. Реализует From, чтобы я мог построить его прямо из массива

Моя самая большая проблема - это реализация Default безопасным и полезным способом. Возможность поддерживать подвижные типы в массиве создала некоторые проблемы. Первоначально я слепо использовал mem::uninitialized(), за которым следовал цикл for ptr::write(&mut data[index], Element::default()) вызовов, чтобы инициализировать его, но я обнаружил, что если default() вызов отдельных элементов когда-либо паникует, то он попытается вызвать drop на всех неинициализированные данные в массиве.

Мой следующий шаг включал использование ящика с нодропом для предотвращения этого. Теперь я больше не вызываю drop для каких-либо неинициализированных данных, но если какой-либо из элементов действительно паникует на default(), то те, что были до него, которые были правильно построены, никогда не вызывают drop.

Есть ли способ сообщить компилятору Rust, что безопасно вызывать drop для предыдущих элементов массива, которые были правильно построены, или есть другой способ приблизиться к этому?

Для ясности, если один из отдельных людей вызывает Element::default() панику, я хочу:

  1. Неинициализированные элементы не вызывают drop
  2. Правильно инициализированные элементы вызывают drop

Я не уверен, что это возможно, основываясь на том, что я прочитал до сих пор, но я хотел проверить.

Этот код показывает, где я нахожусь:

extern crate nodrop;

use nodrop::NoDrop;

struct Dummy;
impl Drop for Dummy {
    fn drop(&mut self) {
        println!("dropping");
    }
}
impl Default for Dummy {
    fn default() -> Self {
        unsafe {
            static mut COUNT: usize = 0;
            if COUNT < 3 {
                COUNT += 1;
                println!("default");
                return Dummy {};
            } else {
                panic!("oh noes!");
            }
        }
    }
}

const CAPACITY: usize = 5;

struct Composite<Element> {
    data: [Element; CAPACITY],
}

impl<Element> Default for Composite<Element>
where
    Element: Default,
{
    fn default() -> Self {
        let mut temp: NoDrop<Self> = NoDrop::new(Self {
            data: unsafe { std::mem::uninitialized() },
        });

        unsafe {
            for index in 0..CAPACITY {
                std::ptr::write(&mut temp.data[index], Element::default());
            }
        }

        return temp.into_inner();
    }
}

impl<Element> From<[Element; CAPACITY]> for Composite<Element> {
    fn from(value: [Element; CAPACITY]) -> Self {
        return Self { data: value };
    }
}

pub fn main() {
    let _v1: Composite<Dummy> = Composite::default();
}

Детская площадка

Это позволяет гарантировать, что неинициализированные элементы массива не вызывают drop, но еще не позволяет правильно инициализированным компонентам вызывать drop (они действуют как неинициализированные компоненты и не вызывают drop). Я заставляю вызов Element::default() вызывать панику на более позднем элементе, чтобы показать проблему.

Фактическая выработка

Стандартная ошибка:

Compiling playground v0.0.1 (file:///playground)
Finished dev [unoptimized + debuginfo] target(s) in 0.56 secs
 Running `target/debug/playground`
thread 'main' panicked at 'oh noes!', src/main.rs:19:17
note: Run with `RUST_BACKTRACE=1` for a backtrace.

Стандартный вывод:

default
default
default

Предполагаемый выход

Стандартная ошибка:

Compiling playground v0.0.1 (file:///playground)
Finished dev [unoptimized + debuginfo] target(s) in 0.56 secs
 Running `target/debug/playground`
thread 'main' panicked at 'oh noes!', src/main.rs:19:17
note: Run with `RUST_BACKTRACE=1` for a backtrace.

Стандартный вывод:

default
default
default
dropped
dropped
dropped

1 Ответ

0 голосов
/ 29 апреля 2018

Есть ли способ указать компилятору Rust вызывать drop для частично инициализированных элементов массива при обработке паники?

Нет , но вы можете позвонить drop самостоятельно. Вам нужно запустить код при возникновении паники.

Решение для перебора

При этом используются строительные блоки catch_unwind, resume_unwind и AssertUnwindSafe, чтобы заметить, что произошла паника, и выполнить некоторый код очистки :

fn default() -> Self {
    use std::panic::{self, AssertUnwindSafe};

    let mut temp = NoDrop::new(Self {
        data: unsafe { std::mem::uninitialized() },
    });

    let mut valid = 0;

    let panicked = {
        let mut temp = AssertUnwindSafe(&mut temp);
        let mut valid = AssertUnwindSafe(&mut valid);

        std::panic::catch_unwind(move || unsafe {
            for index in 0..CAPACITY {
                std::ptr::write(&mut temp.data[index], T::default());
                **valid += 1;
            }
        })
    };

    if let Err(e) = panicked {
        for i in 0..valid {
            unsafe { std::ptr::read(&temp.data[i]) };
        }

        panic::resume_unwind(e);
    }

    temp.into_inner()
}

чуть лучше

Как только вы поймете, что реализация типа Drop запускается при возникновении паники, вы можете использовать это в своих интересах, создав бомбу сбрасывания - тип, который очищается при падении, но в случае успеха путь не сброшен:

extern crate nodrop;

use nodrop::NoDrop;

use std::{mem, ptr};

const CAPACITY: usize = 5;
type Data<T> = [T; CAPACITY];

struct Temp<T> {
    data: NoDrop<Data<T>>,
    valid: usize,
}

impl<T> Temp<T> {
    unsafe fn new() -> Self {
        Self {
            data: NoDrop::new(mem::uninitialized()),
            valid: 0,
        }
    }

    unsafe fn push(&mut self, v: T) {
        if self.valid < CAPACITY {
            ptr::write(&mut self.data[self.valid], v);
            self.valid += 1;
        }
    }

    unsafe fn into_inner(mut self) -> Data<T> {
        let data = mem::replace(&mut self.data, mem::uninitialized());
        mem::forget(self);
        data.into_inner()
    }
}

impl<T> Drop for Temp<T> {
    fn drop(&mut self) {
        unsafe {
            for i in 0..self.valid {
                ptr::read(&self.data[i]);
            }
        }
    }
}

struct Composite<T>(Data<T>);

impl<T> Default for Composite<T>
where
    T: Default,
{
    fn default() -> Self {
        unsafe {
            let mut tmp = Temp::new();

            for _ in 0..CAPACITY {
                tmp.push(T::default());
            }

            Composite(tmp.into_inner())
        }
    }
}

impl<T> From<Data<T>> for Composite<T> {
    fn from(value: Data<T>) -> Self {
        Composite(value)
    }
}

struct Dummy;

impl Drop for Dummy {
    fn drop(&mut self) {
        println!("dropping");
    }
}

impl Default for Dummy {
    fn default() -> Self {
        use std::sync::atomic::{AtomicUsize, Ordering, ATOMIC_USIZE_INIT};

        static COUNT: AtomicUsize = ATOMIC_USIZE_INIT;

        let count = COUNT.fetch_add(1, Ordering::SeqCst);
        if count < 3 {
            println!("default");
            Dummy {}
        } else {
            panic!("oh noes!");
        }
    }
}

pub fn main() {
    let _v1: Composite<Dummy> = Composite::default();
}

Обратите внимание, что я сделал некоторые несвязанные очистки:

  1. Использование атомарной переменной вместо unsafe статических изменяемых переменных.
  2. Не используйте return в качестве последнего оператора блока.
  3. Преобразован Composite в новый тип , так как data не является замечательным именем переменной.
  4. Импортированы модули mem и ptr для облегчения доступа.
  5. Создан псевдоним типа Data<T>, чтобы избежать повторного ввода этой детали.

Элегантное решение

Выбор push во втором решении не случаен. Temp - плохая реализация вектора, выделенного стеком переменного размера. Существует хорошая реализация, называемая arrayvec , которую мы можем использовать вместо:

extern crate arrayvec;

use arrayvec::ArrayVec;

const CAPACITY: usize = 5;
type Data<T> = [T; CAPACITY];

struct Composite<T>(Data<T>);

impl<T> Default for Composite<T>
where
    T: Default,
{
    fn default() -> Self {
        let tmp: ArrayVec<_> = (0..CAPACITY).map(|_| T::default()).collect();

        match tmp.into_inner() {
            Ok(data) => Composite(data),
            Err(_) => panic!("Didn't insert enough values"),
        }
    }
}

Будете ли вы удивлены, узнав, что nodrop был создан в значительной части для использования в arrayvec? Один и тот же автор создал оба!

...