Trait Objects
Generics let one function work with many types, but each call still works
with exactly one of them. A Vec<T> holds only Ts, and a function that
returns impl Trait still returns a single type. Sometimes that isn't enough:
you want a list holding a percentage discount and a fixed-amount discount,
or a function that picks one of several types at runtime.
A trait object, written dyn Trait, is a value of "some type that
implements Trait", where the actual type is only known when the program
runs. This lesson covers &dyn Trait and Box<dyn Trait>, collections and
struct fields of trait objects, returning them from functions, how method calls
on them work (dynamic dispatch), when to choose them over generics, and which
traits can be used as trait objects.
The problem: one list, many types
Here is a trait for price discounts with two implementations:
trait Discount {
fn apply(&self, cents: u32) -> u32;
}
struct PercentOff(u32);
struct AmountOff(u32);
impl Discount for PercentOff {
fn apply(&self, cents: u32) -> u32 {
cents - cents * self.0 / 100
}
}
impl Discount for AmountOff {
fn apply(&self, cents: u32) -> u32 {
cents.saturating_sub(self.0)
}
}
fn main() {
let spring_sale = PercentOff(20);
let coupon = AmountOff(500);
println!("{}", spring_sale.apply(4000));
println!("{}", coupon.apply(4000));
}3200
3500Now try to put both in one vector. A vector's elements must all have the same
type, and PercentOff and AmountOff are different types, even though they
implement the same trait. A generic Vec<T> doesn't help either: T would
have to be one of them. You need a type that means "any Discount".
dyn Trait behind a pointer
That type is dyn Discount. It can't be used on its own, though: a
PercentOff and an AmountOff may have different sizes, so the compiler
can't know how much space a dyn Discount takes. A trait object always lives
behind a pointer, which has a known size: a reference &dyn Discount or a
Box<dyn Discount>.
A &dyn Trait parameter accepts a reference to any implementing type:
trait Discount {
fn apply(&self, cents: u32) -> u32;
}
struct PercentOff(u32);
struct AmountOff(u32);
impl Discount for PercentOff {
fn apply(&self, cents: u32) -> u32 {
cents - cents * self.0 / 100
}
}
impl Discount for AmountOff {
fn apply(&self, cents: u32) -> u32 {
cents.saturating_sub(self.0)
}
}
fn checkout(cents: u32, discount: &dyn Discount) -> u32 {
discount.apply(cents)
}
fn main() {
println!("{}", checkout(2500, &PercentOff(10)));
println!("{}", checkout(2500, &AmountOff(300)));
let pick: &dyn Discount = if 2500 > 2000 {
&PercentOff(10)
} else {
&AmountOff(300)
};
println!("{}", checkout(2500, pick));
}2250
2200
2250Unlike a generic function, checkout is compiled once. It doesn't know which
type it received, only that it has an apply method. That also means the two
branches of the if can produce different types: both are coerced to
&dyn Discount, which is what impl Trait couldn't do.
Box<dyn Trait>: owned trait objects
A reference borrows the value from somewhere else. To own a trait object,
for example to store it in a collection or return it from a function, put it
in a Box. Box::new(value) moves the value to the heap and gives you an
owning pointer to it; the smart pointers lesson covers Box in detail.
Box<dyn Discount> is "an owned, heap-allocated value of some type that
implements Discount":
trait Discount {
fn apply(&self, cents: u32) -> u32;
fn label(&self) -> String;
}
struct PercentOff(u32);
struct AmountOff(u32);
struct FreeShipping;
impl Discount for PercentOff {
fn apply(&self, cents: u32) -> u32 {
cents - cents * self.0 / 100
}
fn label(&self) -> String {
format!("{}% off", self.0)
}
}
impl Discount for AmountOff {
fn apply(&self, cents: u32) -> u32 {
cents.saturating_sub(self.0)
}
fn label(&self) -> String {
format!("{} cents off", self.0)
}
}
impl Discount for FreeShipping {
fn apply(&self, cents: u32) -> u32 {
cents.saturating_sub(499)
}
fn label(&self) -> String {
String::from("free shipping")
}
}
fn main() {
let discounts: Vec<Box<dyn Discount>> = vec![
Box::new(PercentOff(15)),
Box::new(FreeShipping),
Box::new(AmountOff(1000)),
];
let mut price = 10_499;
for discount in &discounts {
price = discount.apply(price);
println!("{:<14} -> {price}", discount.label());
}
}15% off -> 8925
free shipping -> 8426
1000 cents off -> 7426Three different types now sit in one Vec, and the loop calls apply and
label on each without knowing which type it is. Adding a new kind of
discount means writing a new type and implementing the trait; the loop doesn't
change.
The annotation Vec<Box<dyn Discount>> matters: it tells the compiler to turn
each Box<PercentOff>, Box<FreeShipping> and so on into a
Box<dyn Discount>. Without it, the compiler would expect every element to be
the same type as the first.
Returning trait objects
A function whose return type is Box<dyn Trait> can return a different type
from each branch, chosen at runtime. Here a discount code typed by a user is
turned into a discount, or None if the code isn't recognised:
trait Discount {
fn apply(&self, cents: u32) -> u32;
}
struct PercentOff(u32);
struct AmountOff(u32);
impl Discount for PercentOff {
fn apply(&self, cents: u32) -> u32 {
cents - cents * self.0 / 100
}
}
impl Discount for AmountOff {
fn apply(&self, cents: u32) -> u32 {
cents.saturating_sub(self.0)
}
}
fn parse_code(code: &str) -> Option<Box<dyn Discount>> {
if let Some(percent) = code.strip_prefix("PCT") {
let percent: u32 = percent.parse().ok()?;
Some(Box::new(PercentOff(percent)))
} else if let Some(cents) = code.strip_prefix("CUT") {
let cents: u32 = cents.parse().ok()?;
Some(Box::new(AmountOff(cents)))
} else {
None
}
}
fn main() {
for code in ["PCT25", "CUT750", "HELLO", "PCTxyz"] {
match parse_code(code) {
Some(discount) => println!("{code}: {}", discount.apply(3000)),
None => println!("{code}: not a valid code"),
}
}
}PCT25: 2250
CUT750: 2250
HELLO: not a valid code
PCTxyz: not a valid codeThe caller gets a Box<dyn Discount> and never learns whether it holds a
PercentOff or an AmountOff. Compare this with the last mistake in the
generics lesson: impl Discount as the return type would not compile here,
because the two branches return different types.
Trait objects in struct fields
A struct can hold a trait object as a field, which lets you plug a different behaviour into the same struct:
trait Discount {
fn apply(&self, cents: u32) -> u32;
}
struct PercentOff(u32);
struct NoDiscount;
impl Discount for PercentOff {
fn apply(&self, cents: u32) -> u32 {
cents - cents * self.0 / 100
}
}
impl Discount for NoDiscount {
fn apply(&self, cents: u32) -> u32 {
cents
}
}
struct Customer {
name: String,
discount: Box<dyn Discount>,
}
impl Customer {
fn pay(&self, cents: u32) -> u32 {
self.discount.apply(cents)
}
}
fn main() {
let customers = [
Customer {
name: String::from("Ana"),
discount: Box::new(PercentOff(10)),
},
Customer {
name: String::from("Ben"),
discount: Box::new(NoDiscount),
},
];
for customer in &customers {
println!("{} pays {}", customer.name, customer.pay(1200));
}
}Ana pays 1080
Ben pays 1200Both values are Customers, so they fit in one array, but each carries its
own discount. You can also swap the behaviour later by assigning a new box:
customer.discount = Box::new(PercentOff(50)); (on a mut customer).
Static and dynamic dispatch
Generics and trait objects both let code work with "any type that implements a trait", but they call methods differently.
With generics, the compiler makes a copy of the function for each concrete type and calls the right method directly. That is static dispatch: the method is picked at compile time.
With a trait object, the compiler doesn't know the type, so it can't pick the
method in advance. A &dyn Discount or Box<dyn Discount> is a fat
pointer: two pointers, one to the data and one to a vtable, a table
holding the addresses of the type's Discount methods. Calling apply looks
the method up in that table at runtime. That is dynamic dispatch:
use std::fmt::Display;
use std::mem::size_of;
fn main() {
println!("&u32: {}", size_of::<&u32>());
println!("Box<u32>: {}", size_of::<Box<u32>>());
println!("&dyn Display: {}", size_of::<&dyn Display>());
println!("Box<dyn Display>: {}", size_of::<Box<dyn Display>>());
}&u32: 8
Box<u32>: 8
&dyn Display: 16
Box<dyn Display>: 16These are sizes in bytes on a 64-bit machine. A plain reference or Box is
one pointer; a trait object is two, and the extra 8 bytes are the vtable
pointer.
The lookup is cheap, but it stops the compiler from inlining the call, so dynamic dispatch is a little slower. In return you get one compiled function instead of many, and the ability to mix types.
| Use | Generics, impl Trait | Trait objects, dyn Trait |
|---|---|---|
| Types per value | One, fixed at compile time | Any, chosen at runtime |
| Mixed collections | No | Yes, Vec<Box<dyn Trait>> |
| Method calls | Static dispatch, inlinable | Dynamic dispatch through a vtable |
| Compiled code | One copy per type | One copy |
| Methods available | Every method of the bound | Only object-safe methods (see below) |
A good default: reach for generics, and switch to trait objects when you need values of different types side by side, or the type is only known at runtime.
Which traits can be trait objects
Not every trait can be turned into a dyn Trait. A trait object only knows
its methods through the vtable, so every method must be callable without
knowing the concrete type. A trait that allows this is called object safe
(newer compiler versions say dyn compatible). The main rules for its
methods are:
- They don't return
Self. The caller of adynmethod can't know how big the returned value would be. - They have no generic type parameters. A generic method has a separate copy per type, and a vtable can't list infinitely many copies.
- They take
selfin some form (&self,&mut self,self: Box<Self>), not a plain associated function likefn new() -> Self.
A method that breaks a rule can opt out with where Self: Sized. It is then
still available on concrete types, just not through a trait object:
trait Discount {
fn apply(&self, cents: u32) -> u32;
fn doubled(&self) -> Self
where
Self: Sized;
}
struct AmountOff(u32);
impl Discount for AmountOff {
fn apply(&self, cents: u32) -> u32 {
cents.saturating_sub(self.0)
}
fn doubled(&self) -> Self {
AmountOff(self.0 * 2)
}
}
fn main() {
let bigger = AmountOff(100).doubled();
let as_object: Box<dyn Discount> = Box::new(bigger);
println!("{}", as_object.apply(1000));
}800Standard traits like Display, Debug, Iterator and Error are object
safe, which is why you'll meet Box<dyn std::error::Error> as an error type
that can hold any error. Clone is not, because clone returns Self.
Common mistakes
A trait object without a pointer
trait Discount {
fn apply(&self, cents: u32) -> u32;
}
struct PercentOff(u32);
struct AmountOff(u32);
impl Discount for PercentOff {
fn apply(&self, cents: u32) -> u32 {
cents - cents * self.0 / 100
}
}
impl Discount for AmountOff {
fn apply(&self, cents: u32) -> u32 {
cents.saturating_sub(self.0)
}
}
fn weekend_deal(saturday: bool) -> dyn Discount {
if saturday {
return PercentOff(10);
}
AmountOff(200)
}
fn main() {
println!("{}", weekend_deal(true).apply(1000));
}A return value, like a local variable or a Vec element, needs a size known
at compile time, and dyn Discount has none. The compiler offers two fixes.
impl Discount only works when every path returns the same type, which isn't
the case here. The other one does: return Box<dyn Discount> and wrap each
value in Box::new. The same goes for collections: Vec<dyn Discount> fails
the same way, and Vec<Box<dyn Discount>> works.
Leaving out dyn
trait Discount {
fn apply(&self, cents: u32) -> u32;
}
struct AmountOff(u32);
impl Discount for AmountOff {
fn apply(&self, cents: u32) -> u32 {
cents.saturating_sub(self.0)
}
}
fn main() {
let coupon: Box<Discount> = Box::new(AmountOff(100));
println!("{}", coupon.apply(900));
}Older Rust code wrote Box<Discount>; since the 2021 edition the dyn
keyword is required, so it's clear that this is a trait object and not a
type. Write Box<dyn Discount>.
Pushing a value that isn't boxed
trait Discount {
fn apply(&self, cents: u32) -> u32;
}
struct AmountOff(u32);
impl Discount for AmountOff {
fn apply(&self, cents: u32) -> u32 {
cents.saturating_sub(self.0)
}
}
fn main() {
let mut discounts: Vec<Box<dyn Discount>> = Vec::new();
discounts.push(AmountOff(100));
println!("{}", discounts.len());
}The vector holds boxes, so each value has to be put in one first:
discounts.push(Box::new(AmountOff(100)));. The compiler converts the
Box<AmountOff> to a Box<dyn Discount> for you, but it won't box the value
on its own.
Making a trait object from a trait that isn't object safe
trait Discount {
fn apply(&self, cents: u32) -> u32;
fn doubled(&self) -> Self;
}
struct AmountOff(u32);
impl Discount for AmountOff {
fn apply(&self, cents: u32) -> u32 {
cents.saturating_sub(self.0)
}
fn doubled(&self) -> Self {
AmountOff(self.0 * 2)
}
}
fn main() {
let coupon: Box<dyn Discount> = Box::new(AmountOff(100));
println!("{}", coupon.apply(900));
}The compiler points at the method that breaks the rules. Add
where Self: Sized to that method to keep it off the trait object, as in the
example above, or move it to a separate trait.
Summary
dyn Traitis a value of some type that implementsTrait, known only at runtime. It always sits behind a pointer:&dyn Traitto borrow,Box<dyn Trait>to own.Vec<Box<dyn Trait>>holds values of different types side by side, and aBox<dyn Trait>field lets a struct carry pluggable behaviour.- A function returning
Box<dyn Trait>can return a different type from each branch, whichimpl Traitcan't. - Method calls on trait objects use dynamic dispatch through a vtable; generics use static dispatch. Prefer generics unless you need to mix types or pick them at runtime.
- Only object-safe (dyn compatible) traits can be trait objects: no methods
returning
Selfand no generic methods, unless they opt out withwhere Self: Sized.
Practise what you read
Challenges that use the ideas from this lesson.