Other Operators - Drop, Deref, and Fn Traits
A deep dive into Rust's Drop, Deref, DerefMut, and the call operator traits Fn, FnMut, and FnOnce — overloading resource cleanup, dereferencing, and function call syntax.
This group of traits sits apart from the arithmetic and comparison operators. They control three fundamental behaviors that often feel like built-in language features: cleaning up resources when a value goes out of scope, customizing the * (dereference) operator, and making a type callable with () syntax. Each one has a design that reflects Rust’s ownership and borrowing rules.
The Drop Trait
The Drop trait provides a hook that runs automatically when a value’s owner goes out of scope. It is the mechanism behind Rust’s deterministic resource cleanup — sometimes called a destructor.
Unlike Clone or Add, you never call drop directly. The compiler inserts the call to Drop::drop at every point where a value is about to be deallocated.
pub trait Drop {
fn drop(&mut self);
}
The drop method takes &mut self, so it can modify the value and read its fields. It cannot take ownership of the value (the value is still owned by its original scope), but it gets the last chance to interact with the state before the memory is freed.
Drop order is strict:
When a value goes out of scope, its own drop runs first, then each field’s Drop is called in declaration order — not the reverse. That matters if fields rely on each other during cleanup.
Why Drop Exists
Without Drop, every resource that needs explicit release — file handles, network sockets, locks, GPU buffers — would require manual function calls at the end of every code path. That is error-prone and exactly the class of bugs Rust aims to eliminate.
By tying cleanup to ownership, Rust guarantees that resources are released exactly once, even in the presence of early returns, panics, or complex control flow.
Implementing Drop
A type that wraps a file handle and logs when it is cleaned up illustrates the mechanism.
use std::fs::File;
use std::io::Write;
struct Logger<W: Write> {
writer: W,
message: String,
}
impl<W: Write> Drop for Logger<W> {
fn drop(&mut self) {
writeln!(self.writer, "Logger closing: {}", self.message).ok();
}
}
fn main() {
let file = File::create("log.txt").unwrap();
let logger = Logger { writer: file, message: String::from("session ended") };
// ... use logger ...
// `logger` goes out of scope here; Drop::drop runs automatically.
}
When main ends, the Logger is dropped. Its drop method writes a final line to the file, then the fields (writer and message) are dropped — the File closes, and the String frees its heap buffer.
Correct cleanup without explicit calls:
No matter how the function exits, the message will be written. If a panic occurs earlier, the panic unwind path still calls Drop for all in-scope values.
You Cannot Call drop Directly
Rust prevents you from invoking Drop::drop by hand. Doing so would leave the original owner still thinking the value is alive, causing a double-drop.
let x = Logger { ... };
// x.drop(); // compile error: explicit use of destructor method
To force early cleanup, use the std::mem::drop function, which takes ownership of the value and immediately drops it.
let x = Logger { ... };
std::mem::drop(x); // ownership moves to drop(), which drops x
// x is no longer accessible here.
Common Misconceptions
One frequent mistake is assuming that values moved into a Drop implementation are still available after the destructor runs. They are not — when Drop::drop finishes, Rust drops the fields.
Moving out of a type that implements Drop:
If a type implements Drop, you cannot move individual fields out of it with partial moves. The compiler must keep the whole value intact so drop can run later. This can be surprising when trying to destructure a value manually.
How Beginners Should Think About Drop
Think of Drop as a cleanup ticket attached to the value. Wherever the ticket goes, cleanup follows. When the ticket (the owned value) reaches the end of a scope, the cleanup runs. If you pass the ticket to another function, it moves there and runs cleanup at the end of that function. If you want cleanup earlier, you hand the ticket to mem::drop.
The Deref and DerefMut Traits
The * operator in Rust is not hard-coded to raw pointers. It is syntactic sugar for calls to the deref method defined in the Deref trait. This is what lets types like Box<T>, String, and Vec<T> act like references to their inner data.
pub trait Deref {
type Target: ?Sized;
fn deref(&self) -> &Self::Target;
}
pub trait DerefMut: Deref {
fn deref_mut(&mut self) -> &mut Self::Target;
}
Deref::deref returns a reference to some inner type. When you write *x, the compiler translates it to *Deref::deref(&x) — first borrow the outer type, get a reference to the target, then dereference that reference.
Why Deref Exists
Smart pointers like Box<T>, Rc<T>, and Arc<T> own heap-allocated data but need to expose that data as if it were a plain reference. Without Deref, every method call on the inner type would require explicit syntax like (*box).method(). With Deref, you can write box.method() and the compiler inserts the dereference automatically.
A Minimal Custom Smart Pointer
This type wraps a value and behaves like a reference to it.
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> Self {
MyBox(x)
}
}
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
fn main() {
let x = MyBox::new(String::from("hello"));
// Deref coercion allows calling String methods directly.
println!("length: {}", x.len());
// Explicit dereference works too.
assert_eq!(*x, "hello".to_string());
}
When the compiler sees x.len(), it finds no len method on MyBox<String>. It then checks if MyBox<String> implements Deref with a target that has len. It finds Target = String, String has len, so it effectively calls (*x).len().
DerefMut for Mutable Access
Implementing DerefMut allows *x = value to modify the inner data.
use std::ops::{Deref, DerefMut};
impl<T> DerefMut for MyBox<T> {
fn deref_mut(&mut self) -> &mut T {
&mut self.0
}
}
fn main() {
let mut x = MyBox::new(String::from("world"));
x.push_str("!"); // mutable deref coercion
*x = String::from("bye"); // explicit mutable dereference
}
DerefMut cannot give mutable access through a shared reference:
DerefMut::deref_mut takes &mut self, so you can only call it when you already have mutable access to the outer type. There is no way to obtain &mut T from a &MyBox<T> — that would violate Rust’s aliasing rules.
Deref Coercion in Function Arguments
Deref coercion also works when passing references to functions. If a function expects &str, you can pass a &String because String: Deref<Target=str>. The compiler will insert the coercion automatically.
fn print_str(s: &str) {
println!("{}", s);
}
fn main() {
let s = String::from("coercion");
print_str(&s); // &String coerces to &str via Deref
}
Common Mistakes
The most dangerous error is creating an infinite recursion in Deref::deref. If your deref method calls *self or accesses a field through a dereference of itself, the compiler will try to resolve that by calling deref again, leading to unbounded recursion.
// DO NOT WRITE THIS
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&*self // infinite recursion: *self calls deref() again
}
}
Avoid dereferencing self inside Deref::deref:
Use direct field access (&self.0) or raw pointer methods. Never use the * operator on the same type within its own deref implementation unless you are composing with another type that does not loop back.
How Beginners Should Think About Deref
Picture a matryoshka doll. The outer doll holds a smaller one inside. Deref is the method that opens the outer doll and hands you the inner one. Rust can automatically open layers when you try to do something that only the inner doll supports — that is deref coercion. But it will never open more layers than necessary, and it stops at the first matching method.
The Fn, FnMut, and FnOnce Traits
The function call syntax f(args) is also backed by traits. Closures, function pointers, and anything that can be called implement one or more of the Fn, FnMut, and FnOnce traits.
pub trait FnOnce<Args> {
type Output;
extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}
pub trait FnMut<Args>: FnOnce<Args> {
extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}
pub trait Fn<Args>: FnMut<Args> {
extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}
FnOnceis the most general: it can be called at least once. It takesself, consuming the callable.FnMutcan be called multiple times and may mutate its captures. It takes&mut self.Fncan be called multiple times and does not mutate its captures. It takes&self.
Every closure that only borrows its environment immutably implements Fn. One that mutates captured variables implements FnMut. One that moves owned values into itself implements FnOnce.
Why Separate Fn Traits Exist
These three levels mirror Rust’s borrowing rules inside a callable. A closure that captures a &mut reference to a variable cannot implement Fn because calling it through a shared &self would violate the mutable reference’s uniqueness. The trait hierarchy tells the compiler exactly how a callable interacts with its environment, enabling safe concurrency and preventing data races.
Using Fn Traits as Bounds
The most common use of these traits is to accept closures in generic functions.
fn apply_twice<F>(mut f: F, value: i32)
where
F: FnMut(i32) -> i32,
{
let a = f(value);
let b = f(value);
println!("{} then {}", a, b);
}
fn main() {
let multiplier = 3;
apply_twice(|x| x * multiplier, 5);
}
Here multiplier is borrowed immutably, so the closure implements Fn (and therefore FnMut). If the closure had mutated an external variable, we would need FnMut, and if it had moved ownership of a captured value, only FnOnce would apply.
Function pointers implement all three traits:
A plain function pointer fn(i32) -> i32 implements Fn, FnMut, and FnOnce, so you can pass it anywhere one of these traits is expected.
Manual Implementation (Nightly Only)
On stable Rust, you cannot manually implement Fn, FnMut, or FnOnce for your own types. The compiler automatically generates these implementations for closures and function pointers. The feature is unstable because the extern "rust-call" ABI is not finalized.
If you need a custom callable type on stable Rust, you can define a trait with a call method and use it as a bound, or simply accept closures.
Stick to closures for callables:
Attempting to implement Fn manually with #[feature(unboxed_closures)] works on nightly but may break with future compiler changes. For production code, use closures or plain function pointers.
How Beginners Should Think About Fn Traits
Think of a callable as a tiny machine with a button. Fn is the button that never jams — you can press it all day. FnMut has a button that turns a crank as it presses, changing the machine slightly each time. FnOnce is a self-destruct button: it works once and then the machine is gone. Rust checks your closures against these categories so you never press a broken button.
Real-World Context
- Drop is what makes
MutexGuardunlock the mutex when the guard goes out of scope, even if a panic occurs. That pattern — acquiring a resource and tying its release to a guard type’sDrop— is pervasive in idiomatic Rust. - Deref is used by
Box<T>,String,Vec<T>,Cow<'_, str>, and countless other standard types. It enables transparent method access and is a key ingredient in writing ergonomic APIs around owned data. - Fn traits appear in every higher-order function:
Iterator::map,Option::map,thread::spawn, and all combinator chains. They are the glue that makes Rust’s iterator and closure-heavy code work.
Summary
Drop, Deref, and the Fn family each solve a different problem, but they share a common thread: they extend Rust’s syntax into user-defined types without adding language features. Drop ties resource cleanup to ownership, eliminating manual deallocation errors. Deref makes smart pointers feel transparent, letting owned types participate in the borrowing system. Fn/FnMut/FnOnce make any callable object fit into a single type hierarchy, so functions, closures, and future callable types can be passed around generically.
If you find yourself reaching for manual cleanup code, try to wrap the resource in a type with a Drop implementation. If your type holds a value and you want method calls to pass through to it, implement Deref. And when you write a function that takes a closure, choose the most restrictive Fn bound that still compiles — Fn if it only reads captured data, FnMut if it mutates it, and FnOnce if it consumes it.