The Drop Trait Runs Code on Cleanup

The other trait that's important to the smart pointer pattern is the Drop trait. Drop lets us run some code when a value is about to go out of scope. Smart pointers perform important cleanup when being dropped, like deallocating memory or decrementing a reference count. More generally, data types can manage resources beyond memory, like files or network connections, and use Drop to release those resources when our code is done with them. We're discussing Drop in the context of smart pointers, though, because the functionality of the Drop trait is almost always used when implementing smart pointers.

In some other languages, we have to remember to call code to free the memory or resource every time we finish using an instance of a smart pointer. If we forget, the system our code is running on might get overloaded and crash. In Rust, we can specify that some code should be run when a value goes out of scope, and the compiler will insert this code automatically. That means we don't need to remember to put this code everywhere we're done with an instance of these types, but we still won't leak resources!

The way we specify code should be run when a value goes out of scope is by implementing the Drop trait. The Drop trait requires us to implement one method named drop that takes a mutable reference to self.

Listing 15-8 shows a CustomSmartPointer struct that doesn't actually do anything, but we're printing out CustomSmartPointer created. right after we create an instance of the struct and Dropping CustomSmartPointer! when the instance goes out of scope so that we can see when each piece of code gets run. Instead of a println! statement, you'd fill in drop with whatever cleanup code your smart pointer needs to run:

Filename: src/main.rs

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer!");
    }
}

fn main() {
    let c = CustomSmartPointer { data: String::from("some data") };
    println!("CustomSmartPointer created.");
    println!("Wait for it...");
}

Listing 15-8: A CustomSmartPointer struct that implements the Drop trait, where we could put code that would clean up after the CustomSmartPointer.

The Drop trait is in the prelude, so we don't need to import it. The drop method implementation calls the println!; this is where you'd put the actual code needed to close the socket. In main, we create a new instance of CustomSmartPointer then print out CustomSmartPointer created. to be able to see that our code got to that point at runtime. At the end of main, our instance of CustomSmartPointer will go out of scope. Note that we didn't call the drop method explicitly.

When we run this program, we'll see:

CustomSmartPointer created.
Wait for it...
Dropping CustomSmartPointer!

printed to the screen, which shows that Rust automatically called drop for us when our instance went out of scope.

We can use the std::mem::drop function to drop a value earlier than when it goes out of scope. This isn't usually necessary; the whole point of the Drop trait is that it's taken care of automatically for us. We'll see an example of a case when we'll need to drop a value earlier than when it goes out of scope in Chapter 16 when we're talking about concurrency. For now, let's just see that it's possible, and std::mem::drop is in the prelude so we can just call drop as shown in Listing 15-9:

Filename: src/main.rs

fn main() {
    let c = CustomSmartPointer { data: String::from("some data") };
    println!("CustomSmartPointer created.");
    drop(c);
    println!("Wait for it...");
}

Listing 15-9: Calling std::mem::drop to explicitly drop a value before it goes out of scope

Running this code will print the following, showing that the destructor code is called since Dropping CustomSmartPointer! is printed between CustomSmartPointer created. and Wait for it...:

CustomSmartPointer created.
Dropping CustomSmartPointer!
Wait for it...

Note that we aren't allowed to call the drop method that we defined directly: if we replaced drop(c) in Listing 15-9 with c.drop(), we'll get a compiler error that says explicit destructor calls not allowed. We're not allowed to call Drop::drop directly because when Rust inserts its call to Drop::drop automatically when the value goes out of scope, then the value would get dropped twice. Dropping a value twice could cause an error or corrupt memory, so Rust doesn't let us. Instead, we can use std::mem::drop, whose definition is:

pub mod std {
    pub mod mem {
        pub fn drop<T>(x: T) { }
    }
}

This function is generic over any type T, so we can pass any value to it. The function doesn't actually have anything in its body, so it doesn't use its parameter. The reason this empty function is useful is that drop takes ownership of its parameter, which means the value in x gets dropped at the end of this function when x goes out of scope.

Code specified in a Drop trait implementation can be used for many reasons to make cleanup convenient and safe: we could use it to create our own memory allocator, for instance! By using the Drop trait and Rust's ownership system, we don't have to remember to clean up after ourselves since Rust takes care of it automatically. We'll get compiler errors if we write code that would clean up a value that's still in use, since the ownership system that makes sure references are always valid will also make sure that drop only gets called one time when the value is no longer being used.

Now that we've gone over Box<T> and some of the characteristics of smart pointers, let's talk about a few other smart pointers defined in the standard library that add different kinds of useful functionality.

results matching ""

    No results matching ""