Могу ли я привести параметр времени жизни к более короткому сроку жизни (разумно) даже в присутствии `& mut T`? - PullRequest
3 голосов
/ 17 февраля 2020

Я пытаюсь создать дерево с родительскими указателями в 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)
            ))
        }
    }
}

( Playground link )

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

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

... но это нормально с transmuted_child.

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

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

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

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

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

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

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

Ответы [ 2 ]

4 голосов
/ 17 февраля 2020

Ответ # 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 внутри LogNode, которая должна быть 'static. Когда oops возвращается, ссылка больше не действительна, и попытка доступа к ней вызывает неопределенное поведение (segfaulting, для меня).


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

2 голосов
/ 17 февраля 2020

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

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

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

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

Причина этого объясняется в Nomicon (ссылка выше), поэтому я ' Я просто покажу вам (несколько упрощенный) пример, приведенный там. Код Trentcl является рабочим примером того, что пойдет не так, если &'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, поэтому это заставляет два времени жизни к быть равным Это может или не может работать с остальной частью вашего кода, так что возьмите его с крошкой соли.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...