rustmemory-managementsmart-pointers

Why does the Rust compiler drop unused variables in the reverse order they were declared?


In the Rust book an example is provided in chapter 15 section 3 to show when Rust runs the drop function on the variables c and d.

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let c = CustomSmartPointer {
        data: String::from("my stuff"),
    };
    let d = CustomSmartPointer {
        data: String::from("other stuff"),
    };
    println!("CustomSmartPointers created.");
}

and the output is:

CustomSmartPointers created.
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!

My question is why does the rust compiler drop the variables in this order? Given that the variables are not used or referenced shouldn't they be dropped right after they are declared, and be dropped in the order they were declared?

I have also tried the same code with another declared variable.

//snip
let e = CustomSmartPointer {
    data: String::from("more stuff"),
};

and the output stays reversed

CustomSmartPointers created.
Dropping CustomSmartPointer with data `more stuff`!
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!

Although the way variables are dropped reminds of how data is pushed and popped off a stack, but that should be a red herring.


Solution

  • Given that the variables are not used or referenced shouldn't they be dropped right after they are declared

    No. Such behaviour would make RAII impossible. For example this:

    let mut lock: Mutex<i32> = Mutex::new(1);
    ...
    {
        let mut guard = lock.lock().unwrap();
        *guard += 1;
        // Something that doesn't reference guard
        // but needs to be run under lock
        notify_counter_update();
    }
    

    The guard here not only holds data internally, but more importantly it is a structure that unlocks the underlying lock on drop. Thanks to drop happening at the end of the scope I have this entire scope automatically under the lock/unlock process. I don't have to remember about manually unlocking the lock (which would be necessary if Rust dropped guard the moment it is no longer used, I mean: guards would be useless). Which would be very error prone, if the scope contains for example ifs or other nested scopes.

    Another example, consider this:

    fn custom_fn(value: &String) -> &str;
    ...
    
    {
        let x = "foo".to_owned();
        let y = custom_fn(&x);
        println!("{}", y);
    }
    

    when should Rust drop x? The last time it is used, i.e. after call, but before println? What if custom_fn is implemented as

    fn custom_fn(value: &String) -> &str {
        value.as_str()
    }
    

    ? Dropping x before println results in println trying to access deallocated memory (remember that unlike &str the String type owns memory and deallocates it on drop). Undefined Behaviour. Crash, if you are lucky. You could argue that Rust can deduce it. But can it always deduce that? What if:

    fn custom_fn(value: &String) -> &str {
        if rand() > 0.5 {
            value.as_str()
        }
        else
        {
            "baz"
        }
    }
    

    where rand() returns a random float in 0..1 range. What can Rust deduce about this case? Or even worse: what if custom_fn is an external function, not defined in Rust at all? The only correct, consistent and easy to understand way is to drop x at the end of the scope.

    and be dropped in the order they were declared?

    The order is secondary, and in theory any is fine (assuming variables are independent of course). But some choice had to be made, and it is useful to have it consistent. So there are only two natural choices here: the same order as declaration or reversed.

    Reversed order is more natural though. For example back to the example with locks. Assume we have two of them:

    {
        let mut guard1 = lock1.lock().unwrap();
        let mut guard2 = lock2.lock().unwrap();
        // do something
    }
    

    Would you really expect lock1 to be unlocked before lock2? I think most people wouldn't. And in fact it is consistent when nesting scopes. The code above is equivalent to:

    {
        let mut guard1 = lock1.lock().unwrap();
        {
            let mut guard2 = lock2.lock().unwrap();
            // do something
        }
    }
    

    which otherwise (i.e. with different order of drops) wouldn't.