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:
# #![allow(unused_variables)] #fn main() { 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.