Конфликт времен жизни


#1

С реализацией итератора имутабельных значений проблем нет:

impl<'a, V: 'a> Iterator for UnitIter<'a, Joint, V> {
    type Item = (Joint, &'a V);
    fn next(&mut self) -> Option<Self::Item> {
        self.index += 1;
        match self.index {
            1 => Some((Joint::J0, &self.unit.0[0])),
            2 => Some((Joint::J1, &self.unit.0[1])),
            3 => Some((Joint::J2, &self.unit.0[2])),
            4 => Some((Joint::J3, &self.unit.0[3])),
            5 => Some((Joint::J4, &self.unit.0[4])),
            6 => Some((Joint::J5, &self.unit.0[5])),
            7 => Some((Joint::J6, &self.unit.0[6])),
            8 => Some((Joint::J7, &self.unit.0[7])),
            _ => None,
        }
    }
}

а при реализации итератора мутабельных значение возникает конфликт времен жизни:

impl<'a, V: 'a> Iterator for UnitIterMut<'a, Joint, V> {
    type Item = (Joint, &'a mut V);
    fn next(&mut self) -> Option<Self::Item> {
        self.index += 1;
        match self.index {
            1 => Some((Joint::J0, &mut self.unit.0[0])),
            2 => Some((Joint::J1, &mut self.unit.0[1])),
            3 => Some((Joint::J2, &mut self.unit.0[2])),
            4 => Some((Joint::J3, &mut self.unit.0[3])),
            5 => Some((Joint::J4, &mut self.unit.0[4])),
            6 => Some((Joint::J5, &mut self.unit.0[5])),
            7 => Some((Joint::J6, &mut self.unit.0[6])),
            8 => Some((Joint::J7, &mut self.unit.0[7])),
            _ => None,
        }
    }
}

Почему так?
Playground


#2

Потому что компилятор не может гарантировать, что ты не ошибешься в индексах и не напишешь кашу вроде:

1 => Some((Joint::J0, &mut self.unit.0[0])),
2 => Some((Joint::J1, &mut self.unit.0[1])),
3 => Some((Joint::J2, &mut self.unit.0[1])), // copypaste typo
4 => Some((Joint::J3, &mut self.unit.0[3])),

Если ты ошибешься, то будет две мутабельные ссылки на один регион, что является UB.

Твою задачу можно решить через unsafe, посмотри в теме https://users.rust-lang.org/t/implementing-an-iterator-of-mutable-references/8671 , по той же самой причине не может быть iter_mut для оконного итерирования: https://users.rust-lang.org/t/iterator-over-mutable-windows-of-slice/17110/2


#3

Не думаю что тут дело в индексах.
Пока не реализуют GATs такое сделать невозможно (и скорее всего GATs тоже не помогут см. ниже). Я раньше кидал ссылку на статью со способами решения этой проблемы ( Solving the Generalized Streaming Iterator Problem without GATs ), но на практике они все малополезны.
Через unsafe тоже очень не советую это делать. Тут все завязано на особенности мутабельных ссылок - эксклюзивность и инвариантность относительно типа.

Если отбросить лишнее (типаж Iterator) и явно указать вж, функцию next() можно записать так:

fn next<'b, 'a, V>(self: &'b mut UnitIterMut<'a, Joint, V>) -> Option<(Joint, &'a mut V)> {
    Some((Joint::J0, &mut self.unit.0[0]))
}

Если пойти еще дальше все можно свести к такой функции:

fn next<'b, 'a, V>(slf: &'b mut &'a mut V) -> &'a mut V {
    &mut **slf
}

Такая функция в принципе не реализуема, кроме случая когда 'b:'a (вж 'b не меньше чем 'a, в данном случе равно). Я об этом раньше писал в теме: Странное поведение вложенных мутабельных ссылок.
А типаж Iterator без GATs не дает возможности описать связь между вж ассоциированного типа и Self.


#4

Посмотрел как сделан IterMut из стандартной библиотеки и сделал так-же через unsafe:

pub struct IterMut<'a, P, V> {
    unit: *mut Unit<P, V>,
    index: Range<usize>,
    marker: PhantomData<&'a mut Unit<P, V>>,
}

impl<'a, V: 'a> Iterator for IterMut<'a, Joint, V> {
    type Item = (Joint, &'a mut V);
    fn next(&mut self) -> Option<Self::Item> {
        self.index.next().map(|n| {
            (Joint::try_from(n).unwrap(), unsafe {
                &mut (*self.unit).0[n]
            })
        })
    }
}


#5

Похоже я слишком раскатал губу насчет GATs. Они предназначены для случаев когда структура владеет данными, а не ссылается на них.

Даже если GATs позволят связывать вж, как я писал раньше, то такой итератор будет совершенно бесполезен. После первого вызова метода next() произойдет “вечное” заимствование структуры итератора и второй раз его вызвать будет нельзя, никакие ограничения области видимости не помогут. Все из-за одной интересной особенности при вычислении длительности заимствования данных компилятором: Почему переменная data заимствуется после вызова функции?

Так что кроме unsafe вариантов нет.


#6

Я не уверен, что ты все сделал правильно.

Пожалуйста, не делайте так, вообще старайтесь не использовать unsafe, это крайне опасно.


#7

А где я ошибся?

Не думаю что использование сырых указателей,
опаснее чем использование указателей в C/C++,
которыми я тоже пользуюсь.
И почему ребята которые пишут стандартную библиотеку не должны боятся, а я должен?
Вообщем странный совет.


#8

Они боятся. И перепроверяют. И потом находят баги: https://github.com/rust-lang/miri#bugs-found-by-miri

Поэтому то, что ты тут раскидываешься кодом с unsafe может больно ударить по будущим читателям. У меня нет желания перепроверять его корректность, поэтому:

Пожалуйста, не делайте так, вообще старайтесь не использовать unsafe, это крайне опасно.


#9

Как-то это чрезмерно. Некоторые вещи правильно делать только через unsafe.


#10

@mkpankov, считаю правильным не согласиться с последним утверждением.

Очень многое Rust позволяет сделать безопасно, если это делать не совсем так, как это делается в других языках. Здесь правильный подход делать это безопасно и этому мы учим тех, кто знакомится с Rust. Жить не по принципу C(++) “нет, компилятор, брешешь, unsafe даю, безопасно”, а писать безопасный код, даже если он чуть длиннее. С этим, я надеюсь, согласны все.

Иногда встречаются задачи, которые, очевидно, можно выполнить безопасно, но компилятор не даёт этого сделать, потому что он чего-то не знает. Наглядный пример мы видим перед собой: компилятор не знает, что все вызовы next() с гарантией вернут непересекающиеся ссылки. В этот момент возникает сильный соблазн найти пример, как такое где-нибудь обошли, скопировать его вместе с unsafe, не задумываясь особо, и объявить задачу выполненной. И именно в этот момент игнорируется другое радикальное преимущество Rust - возможность построения безопасных абстракций. Если это уже сделали, то это можно скопировать… стоп, а нельзя этим воспользоваться?

Вернёмся к нашему примеру. Проблема в том, что компилятор не в курсе того, что все ссылки берутся на разные элементы. Эта задача уже решена в стандартной библиотеке для случая [T], так зачем писать свой unsafe, когда он уже написан? Давайте попробуем просто им воспользоваться. Итак, у нас было (и не работало) следующее

pub struct UnitIterMut<'a, P, V> {
    unit: &'a mut Unit<P, V>,
    index: usize,
}

impl<'a, V: 'a> Iterator for UnitIterMut<'a, Joint, V> {
    type Item = (Joint, &'a mut V);
    fn next(&mut self) -> Option<Self::Item> {
        self.index += 1;
        match self.index {
            1 => Some((Joint::J0, &mut self.unit.0[0])),
            2 => Some((Joint::J1, &mut self.unit.0[1])),
            3 => Some((Joint::J2, &mut self.unit.0[2])),
            4 => Some((Joint::J3, &mut self.unit.0[3])),
            5 => Some((Joint::J4, &mut self.unit.0[4])),
            6 => Some((Joint::J5, &mut self.unit.0[5])),
            7 => Some((Joint::J6, &mut self.unit.0[6])),
            8 => Some((Joint::J7, &mut self.unit.0[7])),
            _ => None,
        }
    }
}

А стало после переписывания (полная версия на playground)

pub struct UnitIterMut<'a, V> {
    iter: iter::Enumerate<slice::IterMut<'a, V>>,
}

impl<'a, V: 'a> Iterator for UnitIterMut<'a, V> {
    type Item = (Joint, &'a mut V);
    fn next(&mut self) -> Option<Self::Item> {
        let (index, reference) = self.iter.next()?;
        let joint = Joint::from_usize(index)?;
        Some((joint, reference))
    }
}

Здесь метод Joint::from_usize просто переводит из индекса в константу (и вообще сгенерирован при помощи #[derive(FromPrimitive)] из num-derive). Из кода ушло повторение однотипных строк и он теперь работает без unsafe - просто потому, что мы используем многократно протестированный кусочек unsafe-кода, живущий прямо в стандартной библиотеке.

Правило “не пиши unsafe нигде, кроме FFI” для новичков возникает не только потому, что они плохо знают, что именно делать можно, а что нельзя ни в коем случае. Другая немаловажная причина заключается в том, что, если посмотреть вокруг внимательно, зачастую можно найти код, который сделает всё небезопасное за тебя. Мы учимся как сообщество не писать свой unsafe, а искать и использовать существующие примитивы. Так меньше кода, который нужно пристально анализировать на возможность падений или чинить в том случае, если он всё-таки оказывается опасным.

Да, есть ещё и третья важная причина. Бывает порой такое, что ты пытаешься применить какую-то похожую библиотечную абстракцию, а она не встаёт. Не сходится что-то. Смотришь внимательнее и понимаешь, что в твоих условиях так сделать нельзя, потому что упустил какое-то важное условие, и в твоих условиях такой подход был бы чреват падением. При копировании unsafe-кода к себе и доработке напильником пропустить такое куда проще, а искать ошибку с ментальным блоком “это скопировано из надёжного места и, стало быть, тоже надёжно” куда как весело…

Потому я за то, чтобы отвечать спрашивающим в духе “не пиши unsafe нигде, кроме FFI, а лучше и в FFI тоже не пиши, если возможно”. Потому что умение находить и применять подходящие безопасные абстракции тоже очень важно, и чтобы ему учиться, их нужно искать и применять, а не копипастить. Потому что пусть лучше не убивают лайфтайм с помощью transmute, когда знают, что это доживёт до конца программы, а честно пишут Box::leak. Потому что Rust - это язык про то, как собирать безопасные и эффективные абстракции и пользоваться ими.


#11

Это всё хорошо, но главное чтобы такой подход не приводил к тому, что ценой отсутствия unsafe делают свой односвязный список вот такой

pub enum List {
    Empty,
    Elem(i32, Box<List>),
}

В системной разработке на Rust без unsafe никуда, и им надо уметь пользоваться, а не бежать сломя голову и пытаться спрятать тот же unsafe в стандартную библиотеку. Возможно, вы и разрабатываете стандартную библиотеку.


#12

… ценой отсутствия unsafe делают свой односвязный список вот такой…

Я как-то не в курсе, а чем именно он так плох? Ну кроме того, что я бы работал в терминах

struct Node<T> {
    item: T,
    next: Option<Box<Node<T>>>,
}

но вроде бы для односвязного списка, не пытающегося быть thread-safe, это вполне адекватное представление.

Но это был оффтоп. Возвращаясь к серьёзным утверждениям: если передо мной при разработке на Rust встанет необходимость написать односвязный список, я очень серьёзно задумаюсь правильная ли это структура данных и чем меня не устраивают существующие решения. И если есть ответ, почему такой примитив необходим для моего случая и безопасен - я его напишу. По возможности инкапсулировав в отдельный модуль. И записав те требования, от соблюдения которых зависит его безопасность.

Да, есть вещи, которые приходится делать через unsafe. Нет, это не должны быть мелкие кусочки unsafe, раскиданные по тем точкам кода, где возникли проблемы. Иначе со временем обязательно окажется, что парочка фрагментов изначально не гарантирует безопасности (безопасны при “запланированном” использовании, но допускают и сегфолты в safe-коде, если сделать что-то, о чём автор не подумал), а ещё штуки три были безопасны изначально с учётом допущений, которые потом были нарушены при рефакторинге безопасного кода, теперь же небезопасны.

Чуть-чуть перефразируя: unsafe - это разрешение на опасные операции, используемые в примитивах с безопасным интерфейсом, а не “я начальник - ты дурак, мне виднее, компилируй так”.