Порядок уничтожения, связанный с временными в Rust

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

Рассмотрим:

struct A;
impl Drop for A { fn drop(&mut self) { println!("Drop A.") } }

struct B(*const A);
impl Drop for B { fn drop(&mut self) { println!("Drop B.") } }

fn main() {
    let _ = B(&A as *const A); // B is destroyed after this expression itself.
}

Вывод:

Drop B.
Drop A.

Это то, чего вы ожидаете. Но теперь, если вы это сделаете:

fn main() {
    let _b = B(&A as *const A); // _b will be dropped when scope exits main()
}

Вывод:

Drop A.
Drop B.

Это не то, что я ожидал.

Предполагается ли это, и если да, то в чем причина разницы в поведении в двух случаях?

Я использую Rust 1.12.1.

Ответ 1

Временные параметры отбрасываются в конце инструкции, как в С++. Тем не менее, IIRC, порядок уничтожения в Rust не указан (мы увидим последствия этого ниже), хотя текущая реализация, похоже, просто снижает значения в обратном порядке построения.

Там большая разница между let _ = x; и let _b = x;. _ не является идентификатором в Rust: это шаблон шаблона. Поскольку этот шаблон не находит каких-либо переменных, окончательное значение эффективно удаляется в конце инструкции.

С другой стороны, _b является идентификатором, поэтому значение привязано к переменной с этим именем, которая продлевает срок ее службы до конца функции. Однако экземпляр A по-прежнему является временным, поэтому он будет удален в конце инструкции (и я уверен, что С++ сделает то же самое). Поскольку конец инструкции предшествует концу функции, экземпляр A сначала отбрасывается, а экземпляр B отбрасывается вторым.

Чтобы сделать это яснее, добавьте еще один оператор в main:

fn main() {
    let _ = B(&A as *const A);
    println!("End of main.");
}

Это приводит к следующему выводу:

Drop B.
Drop A.
End of main.

Пока все хорошо. Теперь попробуйте с let _b; выход:

Drop A.
End of main.
Drop B.

Как мы видим, Drop B печатается после End of main.. Это демонстрирует, что экземпляр B жив до конца функции, объясняя разные порядки уничтожения.


Теперь посмотрим, что произойдет, если мы изменим B, чтобы взять заимствованный указатель со временем жизни вместо необработанного указателя. На самом деле, отпустите еще один шаг и удалите реализации Drop на мгновение:

struct A;
struct B<'a>(&'a A);

fn main() {
    let _ = B(&A);
}

Это компилируется отлично. За кулисами Rust присваивает одинаковое время жизни экземпляру A и экземпляру B (т.е. Если мы взяли ссылку на экземпляр B, его тип будет &'a B<'a>, где оба 'a являются точно такое же время жизни). Если два значения имеют одинаковое время жизни, то обязательно нужно отбросить одну из них до другой, и, как упоминалось выше, порядок не задан. Что произойдет, если мы добавим реализации Drop?

struct A;
impl Drop for A { fn drop(&mut self) { println!("Drop A.") } }

struct B<'a>(&'a A);
impl<'a> Drop for B<'a> { fn drop(&mut self) { println!("Drop B.") } }

fn main() {
    let _ = B(&A);
}

Теперь мы получаем ошибку компилятора:

error: borrowed value does not live long enough
 --> <anon>:8:16
  |
8 |     let _ = B(&A);
  |                ^ does not live long enough
  |
note: reference must be valid for the destruction scope surrounding statement at 8:4...
 --> <anon>:8:5
  |
8 |     let _ = B(&A);
  |     ^^^^^^^^^^^^^^
note: ...but borrowed value is only valid for the statement at 8:4
 --> <anon>:8:5
  |
8 |     let _ = B(&A);
  |     ^^^^^^^^^^^^^^
help: consider using a `let` binding to increase its lifetime
 --> <anon>:8:5
  |
8 |     let _ = B(&A);
  |     ^^^^^^^^^^^^^^

Поскольку экземпляру A и экземпляру B присвоено одинаковое время жизни, Rust не может рассуждать о порядке уничтожения этих объектов. Ошибка возникает из-за того, что Rust отказывается создавать экземпляр B<'a> со временем жизни самого объекта, когда B<'a> реализует Drop (это правило было добавлено в результате RFC 769 до Rust 1.0). Если это разрешено, Drop сможет получить доступ к уже отброшенным значениям! Однако, если B<'a> не реализует Drop, тогда это разрешено, потому что мы знаем, что ни один код не попытается получить доступ к полям B при удалении структуры.

Ответ 2

Сами указатели не переносят какое-либо время жизни, поэтому компилятор может сделать что-то вроде этого:

  • Пример:

    • B создается (чтобы он мог содержать *const A)
    • A создан
    • B не привязан к привязке и, следовательно, отбрасывается
    • A не требуется и, следовательно, отбрасывается

Позвольте проверить MIR:

fn main() -> () {
    let mut _0: ();                      // return pointer
    let mut _1: B;
    let mut _2: *const A;
    let mut _3: *const A;
    let mut _4: &A;
    let mut _5: &A;
    let mut _6: A;
    let mut _7: ();

    bb0: {
        StorageLive(_1);                 // scope 0 at <anon>:8:13: 8:30
        StorageLive(_2);                 // scope 0 at <anon>:8:15: 8:29
        StorageLive(_3);                 // scope 0 at <anon>:8:15: 8:17
        StorageLive(_4);                 // scope 0 at <anon>:8:15: 8:17
        StorageLive(_5);                 // scope 0 at <anon>:8:15: 8:17
        StorageLive(_6);                 // scope 0 at <anon>:8:16: 8:17
        _6 = A::A;                       // scope 0 at <anon>:8:16: 8:17
        _5 = &_6;                        // scope 0 at <anon>:8:15: 8:17
        _4 = &(*_5);                     // scope 0 at <anon>:8:15: 8:17
        _3 = _4 as *const A (Misc);      // scope 0 at <anon>:8:15: 8:17
        _2 = _3;                         // scope 0 at <anon>:8:15: 8:29
        _1 = B::B(_2,);                  // scope 0 at <anon>:8:13: 8:30
        drop(_1) -> bb1;                 // scope 0 at <anon>:8:31: 8:31
    }

    bb1: {
        StorageDead(_1);                 // scope 0 at <anon>:8:31: 8:31
        StorageDead(_2);                 // scope 0 at <anon>:8:31: 8:31
        StorageDead(_3);                 // scope 0 at <anon>:8:31: 8:31
        StorageDead(_4);                 // scope 0 at <anon>:8:31: 8:31
        StorageDead(_5);                 // scope 0 at <anon>:8:31: 8:31
        drop(_6) -> bb2;                 // scope 0 at <anon>:8:31: 8:31
    }

    bb2: {
        StorageDead(_6);                 // scope 0 at <anon>:8:31: 8:31
        _0 = ();                         // scope 0 at <anon>:7:11: 9:2
        return;                          // scope 0 at <anon>:9:2: 9:2
    }
}

Как мы видим, drop(_1) действительно вызывается до drop(_6), как предполагается, таким образом, вы получаете результат выше.

  1. Пример

В этом примере B привязан к привязке

  • B создается (по той же причине, что и выше)
  • A создан
  • A не связан и опускается
  • B выходит из области действия и падает

Соответствующий MIR:

fn main() -> () {
    let mut _0: ();                      // return pointer
    scope 1 {
        let _1: B;                       // "b" in scope 1 at <anon>:8:9: 8:10
    }
    let mut _2: *const A;
    let mut _3: *const A;
    let mut _4: &A;
    let mut _5: &A;
    let mut _6: A;
    let mut _7: ();

    bb0: {
        StorageLive(_1);                 // scope 0 at <anon>:8:9: 8:10
        StorageLive(_2);                 // scope 0 at <anon>:8:15: 8:29
        StorageLive(_3);                 // scope 0 at <anon>:8:15: 8:17
        StorageLive(_4);                 // scope 0 at <anon>:8:15: 8:17
        StorageLive(_5);                 // scope 0 at <anon>:8:15: 8:17
        StorageLive(_6);                 // scope 0 at <anon>:8:16: 8:17
        _6 = A::A;                       // scope 0 at <anon>:8:16: 8:17
        _5 = &_6;                        // scope 0 at <anon>:8:15: 8:17
        _4 = &(*_5);                     // scope 0 at <anon>:8:15: 8:17
        _3 = _4 as *const A (Misc);      // scope 0 at <anon>:8:15: 8:17
        _2 = _3;                         // scope 0 at <anon>:8:15: 8:29
        _1 = B::B(_2,);                  // scope 0 at <anon>:8:13: 8:30
        StorageDead(_2);                 // scope 0 at <anon>:8:31: 8:31
        StorageDead(_3);                 // scope 0 at <anon>:8:31: 8:31
        StorageDead(_4);                 // scope 0 at <anon>:8:31: 8:31
        StorageDead(_5);                 // scope 0 at <anon>:8:31: 8:31
        drop(_6) -> [return: bb3, unwind: bb2]; // scope 0 at <anon>:8:31: 8:31
    }

    bb1: {
        resume;                          // scope 0 at <anon>:7:1: 9:2
    }

    bb2: {
        drop(_1) -> bb1;                 // scope 0 at <anon>:9:2: 9:2
    }

    bb3: {
        StorageDead(_6);                 // scope 0 at <anon>:8:31: 8:31
        _0 = ();                         // scope 1 at <anon>:7:11: 9:2
        drop(_1) -> bb4;                 // scope 0 at <anon>:9:2: 9:2
    }

    bb4: {
        StorageDead(_1);                 // scope 0 at <anon>:9:2: 9:2
        return;                          // scope 0 at <anon>:9:2: 9:2
    }
}

Как мы видим, drop(_6) вызывается до drop(_1), поэтому мы получаем поведение, которое вы видели.