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

use core::mem::transmute;

pub struct LogNode<'n>(Option<&'n mut LogNode<'n>>);

impl<'n> LogNode<'n> {
    pub fn child<'a>(self: &'a mut LogNode<'n>) -> LogNode<'a> {
        LogNode(Some(self))
    }

    pub fn transmuted_child<'a>(self: &'a mut LogNode<'n>) -> LogNode<'a> {
        unsafe {
            LogNode(Some(
                transmute::<&'a mut LogNode<'n>, &'a mut LogNode<'a>>(self)
            ))
        }
    }
}

(Ссылка на игровую площадку)

Руст жалуется на child ...

ошибка [E0495]: невозможно определить подходящее время жизни для параметра времени жизни 'n из-за противоречивых требований

... но с transmuted_child все в порядке.

Думаю, я понимаю, почему child не компилируется: тип параметра self - &'a mut LogNode<'n>, но дочерний узел содержит &'a mut LogNode<'a>, а Rust не хочет принудительно { {X4}} на LogNode<'a>. Если я изменю изменяемые ссылки на общие ссылки, компилируется отлично a>, поэтому похоже, что изменяемые ссылки представляют собой проблему именно потому, что &mut T инвариантен по отношению к T (тогда как &T ковариантен). Я предполагаю, что изменяемая ссылка в LogNode всплывает, чтобы сделать сам LogNode инвариантным в течение его параметра времени жизни.

Но я не понимаю, почему это так - интуитивно кажется, что совершенно правильно взять LogNode<'n> и сократить время жизни его содержимого, превратив его в LogNode<'a>. Так как срок жизни не продлевается, невозможно получить доступ к значению по истечении срока его службы, и я не могу думать о каком-либо другом неправильном поведении, которое может произойти.

transmuted_child позволяет избежать проблем с продолжительностью жизни, потому что это обходит проверку заимствований, но я не знаю, является ли использование небезопасного Rust правильным, и даже если это так, я бы предпочел использовать безопасный Rust, если это возможно. Могу я?

Я могу придумать три возможных ответа на этот вопрос:

  1. child может быть полностью реализован в безопасном Rust, и вот как.
  2. child не может быть полностью реализован в Safe Rust, но transmuted_child является надежным.
  3. child не может быть полностью реализован в Safe Rust, и transmuted_child не имеет смысла.

Редактировать 1: Исправлено утверждение, что &mut T инвариантен в течение всего срока действия ссылки. (Не правильно читал nomicon.)

Редактировать 2: Исправлено мое первое редактирование резюме.

3
ashtneoi 17 Фев 2020 в 03:10

2 ответа

Лучший ответ

Ответ № 3: child не может быть реализован в Safe Rust, а transmuted_child не имеет смысла¹. Вот программа, которая использует transmuted_child (и никакой другой unsafe код), чтобы вызвать segfault:

fn oops(arg: &mut LogNode<'static>) {
    let mut short = LogNode(None);
    let mut child = arg.transmuted_child();
    if let Some(ref mut arg) = child.0 {
        arg.0 = Some(&mut short);
    }
}

fn main() {
    let mut node = LogNode(None);
    oops(&mut node);
    println!("{:?}", node);
}

short - кратковременная локальная переменная, но, поскольку вы можете использовать transmuted_child для сокращения параметра времени жизни LogNode, вы можете вставить ссылку на short внутри { {X4}} это должно быть 'static. Когда oops возвращается, ссылка больше не действительна, и попытка доступа к ней вызывает неопределенное поведение (segfaulting, для меня).


¹ В этом есть некоторая тонкость. Это правда, что transmuted_child сам не имеет неопределенного поведения, но поскольку это делает возможным другой код, такой как oops, его вызов или раскрытие могут сделать ваш интерфейс неработоспособным. Чтобы представить эту функцию как часть безопасного API, вы должны быть очень осторожны, чтобы не предоставлять другие функции, которые позволили бы пользователю написать что-то вроде oops. Если вы не можете этого сделать и не можете избежать написания transmuted_child, это следует сделать unsafe fn.

4
trentcl 17 Фев 2020 в 15:27

Чтобы понять, почему работает неизменяемая версия, а изменяемая версия не работает (как написано), нам нужно обсудить Подтип и дисперсия.

Ржавчина в основном не имеет подтипов. Значения обычно имеют уникальный тип. Однако в Rust действительно есть подтипы - время жизни. Если 'a: 'b (read 'a длиннее, чем 'b), то, например, &'a T является подтипом &'b T, интуитивно, поскольку можно обрабатывать более длительные времена жизни как будто они были короче.

Дисперсия - это способ распространения подтипов. Если A является подтипом B, и у нас есть универсальный тип Foo<T>, Foo<A> может быть подтипом Foo<B>, или наоборот. В первом случае, когда направление подтипа остается неизменным, Foo<T> называется ковариантным относительно T. Во втором случае, когда направление меняется на противоположное, говорят, что оно является контрвариантным, а в третьем случае оно называется инвариантным.

Для этого случая соответствующие типы: &'a T и &'a mut T. Оба являются ковариантными в 'a (поэтому ссылки с более длинными временами жизни можно привести к ссылкам с более короткими временами жизни). &'a T ковариантен в T, но &'a mut T является инвариантом в T.

Причина этого объясняется в Nomicon (ссылка выше), поэтому я просто покажу вам (несколько упрощенный) пример, приведенный там. Код Тренткла является рабочим примером того, что пойдет не так, если &'a mut T ковариантен в T.

fn evil_feeder(pet: &mut Animal) {
    let spike: Dog = ...;

    // `pet` is an Animal, and Dog is a subtype of Animal,
    // so this should be fine, right..?
    *pet = spike;
}

fn main() {
    let mut mr_snuggles: Cat = ...;
    evil_feeder(&mut mr_snuggles);  // Replaces mr_snuggles with a Dog
    mr_snuggles.meow();             // OH NO, MEOWING DOG!
}

Так почему же работает неизменяемая версия child, а не изменяемая версия? В неизменной версии LogNode содержит неизменную ссылку на LogNode, поэтому по ковариации как времени жизни, так и параметра типа, LogNode является ковариантным по своему параметру времени жизни. Если 'a: 'b, то LogNode<'a> является подтипом LogNode<'b>.

У нас есть self: &'a LogNode<'n>, что подразумевает 'n: 'a (в противном случае это заимствование превзойдет данные в LogNode<'n>). Таким образом, поскольку LogNode ковариантен, LogNode<'n> является подтипом LogNode<'a>. Кроме того, ковариация в неизменяемых ссылках снова позволяет &'a LogNode<'n> быть подтипом &'a LogNode<'a>. Таким образом, self: &'a LogNode<'n> может быть приведен к &'a LogNode<'a> по мере необходимости для возвращаемого типа в child.

Для изменяемой версии LogNode<'n> не является ковариантным в 'n. Дисперсия здесь сводится к дисперсии &'n mut LogNode<'n>. Но поскольку здесь есть время жизни в T "части изменяемой ссылки, инвариантность изменяемой ссылки (в T) подразумевает, что это также должно быть инвариантным.

Все это в совокупности показывает, что self: &'a mut LogNode<'n> нельзя принудить к &'a mut LogNode<'a>. Итак, функция не компилируется.


Одним из решений этого является добавление границы времени жизни 'a: 'n, хотя, как отмечалось выше, у нас уже есть 'n: 'a, поэтому это приводит к тому, что эти два времени жизни равны. Это может или не может работать с остальной частью вашего кода, так что возьмите его с собой.

2
SCappella 17 Фев 2020 в 02:36