Lesson 16 of 22ยท15 min

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
3500

Now 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
2250

Unlike 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 -> 7426

Three 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 code

The 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 1200

Both 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>: 16

These 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.

UseGenerics, impl TraitTrait objects, dyn Trait
Types per valueOne, fixed at compile timeAny, chosen at runtime
Mixed collectionsNoYes, Vec<Box<dyn Trait>>
Method callsStatic dispatch, inlinableDynamic dispatch through a vtable
Compiled codeOne copy per typeOne copy
Methods availableEvery method of the boundOnly 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 a dyn method 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 self in some form (&self, &mut self, self: Box<Self>), not a plain associated function like fn 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));
}
800

Standard 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 Trait is a value of some type that implements Trait, known only at runtime. It always sits behind a pointer: &dyn Trait to borrow, Box<dyn Trait> to own.
  • Vec<Box<dyn Trait>> holds values of different types side by side, and a Box<dyn Trait> field lets a struct carry pluggable behaviour.
  • A function returning Box<dyn Trait> can return a different type from each branch, which impl Trait can'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 Self and no generic methods, unless they opt out with where Self: Sized.

Practise what you read

Challenges that use the ideas from this lesson.