Item 11: Implement the Drop
trait for RAII patterns
"Never send a human to do a machine's job." – Agent Smith
RAII stands for "Resource Acquisition Is Initialization" which is a programming pattern where the lifetime of a value is exactly tied to the lifecycle of some additional resource. The RAII pattern was popularized by the C++ programming language and is one of C++'s biggest contributions to programming.
The correlation between the lifetime of a value and the lifecycle of a resource is encoded in an RAII type:
- The type's constructor acquires access to some resource
- The type's destructor releases access to that resource
The result of this is that the RAII type has an invariant: access to the underlying resource is available if and only if the item exists. Because the compiler ensures that local variables are destroyed at scope exit, this in turn means that the underlying resources are also released at scope exit.1
This is particularly helpful for maintainability: if a subsequent change to the code alters the control flow, item and
resource lifetimes are still correct. To see this, consider some code that manually locks and unlocks a mutex, without
using the RAII pattern; this code is in C++, because Rust's Mutex
doesn't allow this kind of error-prone usage!
// C++ code
class ThreadSafeInt {
public:
ThreadSafeInt(int v) : value_(v) {}
void add(int delta) {
mu_.lock();
// ... more code here
value_ += delta;
// ... more code here
mu_.unlock();
}
A modification to catch an error condition with an early exit leaves the mutex locked:
// C++ code
void add_with_modification(int delta) {
mu_.lock();
// ... more code here
value_ += delta;
// Check for overflow.
if (value_ > MAX_INT) {
// Oops, forgot to unlock() before exit
return;
}
// ... more code here
mu_.unlock();
}
However, encapsulating the locking behavior into an RAII class:
// C++ code (real code should use std::lock_guard or similar)
class MutexLock {
public:
MutexLock(Mutex* mu) : mu_(mu) { mu_->lock(); }
~MutexLock() { mu_->unlock(); }
private:
Mutex* mu_;
};
means the equivalent code is safe for this kind of modification:
// C++ code
void add_with_modification(int delta) {
MutexLock with_lock(&mu_);
// ... more code here
value_ += delta;
// Check for overflow.
if (value_ > MAX_INT) {
return; // Safe, with_lock unlocks on the way out
}
// ... more code here
}
In C++, RAII patterns were often originally used for memory management, to ensure that manual allocation (new
,
malloc()
) and deallocation (delete
, free()
) operations were kept in sync. A general version of this memory
management was added to the C++ standard library in C++11: the std::unique_ptr<T>
type ensures that a single
place has "ownership" of memory but allows a pointer to the memory to be "borrowed" for ephemeral use
(ptr.get()
).
In Rust, this behavior for memory pointers is built into the language (Item 15), but the general principle of RAII is
still useful for other kinds of resources.2 Implement Drop
for any
types that hold resources that must be released, such as the following:
- Access to operating system resources. For Unix-derived systems, this usually means something that holds a file descriptor; failing to release these correctly will hold onto system resources (and will also eventually lead to the program hitting the per-process file descriptor limit).
- Access to synchronization resources. The standard library already includes memory synchronization primitives, but other resources (e.g., file locks, database locks, etc.) may need similar encapsulation.
- Access to raw memory, for
unsafe
types that deal with low-level memory management (e.g., for foreign function interface [FFI] functionality).
The most obvious instance of RAII in the Rust standard library is the
MutexGuard
item returned by
Mutex::lock()
operations, which tend to be widely
used for programs that use the shared-state parallelism discussed in Item 17. This is roughly analogous to the
final C++ example shown earlier, but in Rust the MutexGuard
item acts as a proxy to the mutex-protected data in addition to
being an RAII item for the held lock:
#![allow(unused)] fn main() { use std::sync::Mutex; struct ThreadSafeInt { value: Mutex<i32>, } impl ThreadSafeInt { fn new(val: i32) -> Self { Self { value: Mutex::new(val), } } fn add(&self, delta: i32) { let mut v = self.value.lock().unwrap(); *v += delta; } } }
Item 17 advises against holding locks for large sections of code; to ensure this, use blocks to restrict the scope of RAII items. This leads to slightly odd indentation, but it's worth it for the added safety and lifetime precision:
impl ThreadSafeInt {
fn add_with_extras(&self, delta: i32) {
// ... more code here that doesn't need the lock
{
let mut v = self.value.lock().unwrap();
*v += delta;
}
// ... more code here that doesn't need the lock
}
}
Having proselytized the uses of the RAII pattern, an explanation of how to implement it is in order. The
Drop
trait allows you to add user-defined behavior to the
destruction of an item. This trait has a single method,
drop
, which the compiler runs just before the
memory holding the item is released:
#![allow(unused)] fn main() { #[derive(Debug)] struct MyStruct(i32); impl Drop for MyStruct { fn drop(&mut self) { println!("Dropping {self:?}"); // Code to release resources owned by the item would go here. } } }
The drop
method is specially reserved for the compiler and can't be manually invoked:
x.drop();
error[E0040]: explicit use of destructor method
--> src/main.rs:70:7
|
70 | x.drop();
| --^^^^--
| | |
| | explicit destructor calls not allowed
| help: consider using `drop` function: `drop(x)`
It's worth understanding a little bit about the technical details here. Notice that the Drop::drop
method has a
signature of drop(&mut self)
rather than drop(self)
: it takes a mutable reference to the item rather than having the
item moved into the method. If Drop::drop
acted like a normal method, that would mean the item would still be
available for use afterward—even though all of its internal state has been tidied up and resources released!
{
// If calling `drop` were allowed...
x.drop(); // (does not compile)
// `x` would still be available afterwards.
x.0 += 1;
}
// Also, what would happen when `x` goes out of scope?
The compiler suggested a straightforward alternative, which is to call the
drop()
function to manually drop an item. This
function does take a moved argument, and the implementation of drop(_item: T)
is just an empty body { }
—so
the moved item is dropped when that scope's closing brace is reached.
Notice also that the signature of the drop(&mut self)
method has no return type, which means that it has no way to
signal failure. If an attempt to release resources can fail, then you should probably have a separate release
method
that returns a Result
, so it's possible for users to detect this failure.
Regardless of the technical details, the drop
method is nevertheless the key place for implementing RAII patterns; its
implementation is the ideal place to release resources associated with an item.
This also means that RAII as a technique
is mostly available only in languages that have a predictable time of destruction, which rules out most
garbage-collected languages (although Go's defer
statement achieves some of the same ends).