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"; this 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.
With an RAII type,
- the type's constructor acquires access to some resource, and
- 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 when at scope exit, this in turn means that the underlying resources are also released at scope exit1.
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;
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 behaviour into an RAII class:
// C++ code
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 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 which allows a pointer to the memory to be "borrowed" for ephemeral use (ptr.get()
).
In Rust, this behaviour for memory pointers is built into the language (Item 14), but the general principle of RAII is
still useful for other kinds of resources2. Implement Drop
for any types that hold
resources that must be released, such as:
- 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 on to 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, …) may need similar encapsulation.
- Access to raw memory, for
unsafe
types that deal with low-level memory management (e.g. for FFI).
The most obvious instance of RAII in the Rust standard library is the
MutexGuard
item returned by
Mutex::lock()
operations (should you choose to
ignore the advice of Item 17 and use shared-state parallelism). This is roughly analogous to the final C++ example
above, but here the MutexGuard
item acts as a proxy to the mutex-protected data in addition to being an RAII item
for the held lock:
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.
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 behaviour 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.
#[derive(Debug)]
struct MyStruct(i32);
impl Drop for MyStruct {
fn drop(&mut self) {
println!("Dropping {:?}", self);
}
}
The drop
method is specially reserved to the compiler and can't be manually invoked, because the item would be
left in a potentially messed-up state afterwards:
x.drop();
error[E0040]: explicit use of destructor method
--> raii/src/main.rs:63:7
|
63 | x.drop();
| ^^^^
| |
| explicit destructor calls not allowed
| help: consider using `drop` function: `drop(x)`
(As suggested by the compile, just call drop(obj)
instead to
manually drop an item.)
The drop
method is therefore the key place for implementing RAII patterns, by ensuring that resources are released on
item destruction.
1: This also means that RAII as a technique
is mostly only available 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.)
2: RAII is also still useful for memory management in low-level
unsafe
code, but that is (mostly) beyond the scope of this book