Why a matched value gets dropped?

Hi,

I am having my own portion of lifetime/borrow checker problems.

The code:

struct Ref<'a, T>(&'a mut T);
impl<'a, T> Drop for Ref<'a, T> {
    fn drop(&mut self) { }
}

fn maybe_consume<'a, T>(consume: bool, value: Ref<'a, T>) -> Option<Ref<'a, T>> {
    if consume { None } else { Some(value) }
}

fn maybe_consume_reset<'a, T: Default>(consume: bool, value: Ref<'a, T>) -> Ref<'a, T> {
    let reb = Ref(&mut *value.0);  // The borrow starts here (it should be a reborrow for a shorter lifetime right?)
    let temp = maybe_consume(consume, reb);  // ... here it is transferred to `temp` (`reb` is consumed by move by value)
    let res = match temp {
        Some(r) => {  // here it is transferred to `r` (`temp` is consumed by match by value)
            drop(r);
            // The borrow should end here
            value
        }
        None => {   // here it should end (`temp` is consumed by match by value)
            // The borrow should end here
            *value.0 = Default::default();
            value
        }
    };
    // There should be nothing to drop here, `temp` was fully destructured by match
    res
}

The error:

error[E0505]: cannot move out of `value` because it is borrowed
  --> src/main.rs:28:13
   |
21 | fn maybe_consume_reset<'a, T: Default>(consume: bool, value: Ref<'a, T>) -> Ref<'a, T> {
   |                                                       ----- binding `value` declared here
22 |     let reb = Ref(&mut *value.0);  // The borrow starts here (it should be a reborrow for a shorter lifetime right?)
   |                   ------------- borrow of `*value.0` occurs here
...
28 |             value
   |             ^^^^^ move out of `value` occurs here
...
38 | }
   | - borrow might be used here, when `temp` is dropped and runs the destructor for type `Option<Ref<'_, T>>`

error[E0506]: cannot assign to `*value.0` because it is borrowed
  --> src/main.rs:32:13
   |
22 |     let reb = Ref(&mut *value.0);  // The borrow starts here (it should be a reborrow for a shorter lifetime right?)
   |                   ------------- `*value.0` is borrowed here
...
32 |             *value.0 = Default::default();
   |             ^^^^^^^^ `*value.0` is assigned to here but it was already borrowed
...
38 | }
   | - borrow might be used here, when `temp` is dropped and runs the destructor for type `Option<Ref<'_, T>>`

My main questions are:

  1. Where are my comments incorrect?
  2. Why the "borrow might be used here, when temp is dropped ..."? The temp should be (at least I think so) fully consumed by match. Is there a way to convince the compiler there is always nothing to drop?

You can use ManuallyDrop as an escape hatch to relax drop check. Drop Check - The Rustonomicon

fn maybe_consume<'a, T>(
    consume: bool,
    value: ManuallyDrop<Ref<'a, T>>,
) -> Option<ManuallyDrop<Ref<'a, T>>> {
    if consume {
        None
    } else {
        Some(value)
    }
}

fn maybe_consume_reset<'a, T: Default>(consume: bool, value: Ref<'a, T>) -> Ref<'a, T> {
    let reb = ManuallyDrop::new(Ref(&mut *value.0));
    let temp = maybe_consume(consume, reb);
    let res = match temp {
        Some(mut r) => {
            unsafe { ManuallyDrop::drop(&mut r) };
            value
        }
        None => {
            *value.0 = Default::default();
            value
        }
    };
    res
}

Rust Playground

But since drop for reference type is meaningless, you can just simplify as follows

fn maybe_consume_reset<'a, T: Default>(consume: bool, value: Ref<'a, T>) -> Ref<'a, T> {
    let reb = ManuallyDrop::new(Ref(&mut *value.0));
    let temp = maybe_consume(consume, reb);
    if temp.is_none() {
        *value.0 = Default::default();
    }
    value
}

Alternatively, another escape hatch is #[may_dangle]: Rust Playground

#![feature(dropck_eyepatch)]
struct Ref<'a, T>(&'a mut T);

unsafe impl<#[may_dangle] 'a, T> Drop for Ref<'a, T> {
    fn drop(&mut self) {}
}

// rest of your code

Matching against None doesn't move temp... and matching Some(r) is considered a partial move of temp, apparently.

This works:

    let res = match temp {
        Some(_) => {  
            drop(temp);
            value
        }
        None => {
            drop(temp);
            *value.0 = Default::default();
            value
        }
    };

(Unless you're just exploring how things work, I think we're missing some context. Why does Ref have a Drop implementation?)

1 Like

It is not entirely clear to me why your first example works. Just a guess: is it because ManuallyDrop has a trivial destructor so no drop glue for temp is inserted after the match?

I do not understand why this is not sufficient:

    let res = match temp {
        Some(r) => {
            drop(r);
            value
        }
        None => {
            drop(temp);
            *value.0 = Default::default();
            value
        }
    };

In one branch temp is partially moved, so it cannot be dropped later (and its parts are dropped in that branch) and in the other branch it is dropped. What can possibly happen to temp after the match?

And yes, I am now mainly exploring and trying to understand how things work. I was originally working on something, encountered an error and in an attempt to resolve it I played around and tried to simplify my code to find the cause until I found something beyond my understanding.

If you really wish, I can share my code unmodified, but I am explicitly not asking for help with my original problem. While it might be convenient to tell what I want and let you do all the hard work, I would rather understand how things work and figure my problems out by myself ... now and anytime later. (Maybe I should put the topic into a different category?)

So the question remains: Why temp gets dropped at the point where it is provably either already dropped or deconstructed?

1 Like

Yes. ManuallyDrop has no drop glue.

ManuallyDrop
A wrapper to inhibit compiler from automatically calling T ’s destructor. This wrapper is 0-cost.

But you should also check out the nomicon link on dropck and ManuallyDrop's doc.

:+1:

Here's an issue about it.

When you partially move out of something with drop glue, some drop glue must still run at the destruction point at least sometimes. It may need to drop other fields that you didn't move, for example. Like in this playground. I hadn't really thought much about it before, but partially moving out of an enum variant also needs to "lock in" which variant the partially moved value is.

Like my previous playground shows, if you give the compiler enough information, it eliminates the end-of-scope destruction spot -- like if you unconditionally move the value on all branches. In the case where you move Some(r /* <--- moved */) but not temp, that clearly isn't happening. I'd have to play around to see if this ever happens (before optimization) when partial moves are involved (i.e. "all fields were partially moved so no reason for the drop glue of the original value to run"). Or maybe this has more to do with enum variants, I'm not sure.

The issue calls it "an bug with borrow checking", but I'd probably call it more of a shortcoming of drop checking or such. The borrow checker can't change where things drop; that information is just an input to the borrow checker.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.