Box<dyn Trait>
Box<dyn Trait> is an owned, single-owner heap-allocated trait object that stores a concrete value on the heap and holds a 16-byte fat pointer (data_ptr + vtable_ptr) on the stack.
Memory Layout and Ownership
Unlike &dyn Trait, which borrows a value tied to a stack frame's lifetime, Box<dyn Trait> owns the underlying value on the heap and can be moved freely between functions, stored in structs, or kept for the lifetime of the program:
Stack (`Box<dyn Storage>`, 16 B) Heap Allocation
+------------------+ +------------------------------+
| data_ptr (8 B) -+---------------> | DiskStorage { root: "/data" }|
+------------------+ +------------------------------+
| vtable_ptr (8 B)-+---> [vtable for DiskStorage as Storage]
+------------------+
When Box<dyn Trait> goes out of scope, its Drop implementation calls the drop_in_place function pointer stored in the vtable to run the concrete type's destructor, and then deallocates the heap block using the size and align stored in the vtable.
Storing Owned Trait Objects in Struct Fields
trait Compressor {
fn algorithm(&self) -> &str;
fn compress(&self, data: &[u8]) -> Vec<u8>;
}
struct GzipCompressor {
level: u8,
}
impl Compressor for GzipCompressor {
fn algorithm(&self) -> &str {
"gzip"
}
fn compress(&self, data: &[u8]) -> Vec<u8> {
let mut out = vec![self.level];
out.extend_from_slice(data);
out
}
}
struct ArchiveWriter {
compressor: Box<dyn Compressor>,
}
impl ArchiveWriter {
fn new(compressor: Box<dyn Compressor>) -> Self {
Self { compressor }
}
fn write_chunk(&self, chunk: &[u8]) {
let compressed = self.compressor.compress(chunk);
println!(
"Compressed {} bytes with {} -> {:02X?}",
chunk.len(),
self.compressor.algorithm(),
compressed
);
}
}
fn main() {
let writer = ArchiveWriter::new(Box::new(GzipCompressor { level: 6 }));
writer.write_chunk(&[0xAA, 0xBB, 0xCC]);
}
Compressed 3 bytes with gzip -> [06, AA, BB, CC]
Consuming a Box<dyn Trait> by Value (self: Box<Self>)
Normally, a trait object method cannot take self by value (fn into_inner(self)) because dyn Trait is unsized. However, a trait method can take self: Box<Self> as its receiver, allowing callers to consume and move out of a Box<dyn Trait> through dynamic dispatch:
trait ConsumableTask {
fn run_once(self: Box<Self>) -> String;
}
struct UploadJob {
filename: String,
}
impl ConsumableTask for UploadJob {
fn run_once(self: Box<Self>) -> String {
// `self` is `Box<UploadJob>`, so we can move `self.filename` out by value:
self.filename
}
}
fn main() {
let job: Box<dyn ConsumableTask> = Box::new(UploadJob {
filename: String::from("report.parquet"),
});
let finished_file = job.run_once();
println!("Uploaded: {finished_file}");
}
Uploaded: report.parquet
Shared Ownership Alternatives (Rc<dyn Trait> and Arc<dyn Trait>)
When multiple owners need to share the same heap-allocated trait object without cloning the underlying value:
- Use
Rc<dyn Trait>in single-threaded code. - Use
Arc<dyn Trait + Send + Sync>in multi-threaded code.