2024-03-15

Dynamic Dispatch of Traits with Methods that take Ownership

Bumped into this the other day! Suppose you have a trait method that takes ownership of self in a case something like this:

trait Enchantment {
    fn enchant(self, character: &mut Character);
}

struct Frogify;

impl Enchantment for Frogify {
    fn enchant(self, character: &mut Character) {
        character.strength = 1;
    }
}

struct Character {
    strength: u32
}

fn main() {
    let mut prince = Character { strength: 10 };
    let enchantment = Frogify {};
    enchantment.enchant(&mut prince);
    println!("prince has strength {}", prince.strength);
}

everything here works fine! But now suppose you make a function that returns a random enchantment:

fn random_enchantment() -> Box<dyn Enchantment> {
    // not very random, but could be!
    Box::new(Frogify {})
}

and decide to try to enchant the prince with a random enchantment:

fn main() {
    let mut prince = Character { strength: 10 };
    let enchantment = random_enchantment();
    enchantment.enchant(&mut prince);
    println!("prince has strength {}", prince.strength);
}

you’ll get this error:

error[E0161]: cannot move a value of type `dyn Enchantment`
  --> src/main.rs:24:5
   |
24 |     enchantment.enchant(&mut prince);
   |     ^^^^^^^^^^^ the size of `dyn Enchantment` cannot be statically determined

To call the function enchant, rust tries to dereference the boxed enchantment value but can’t do so because of it having an unknown size! One option is to just not consume the enchantment on use, but that might be a nice feature if the operation is expensive, or semantically describes what is being done in a nice way (enchantments might be one use only). So then, How can we represent an owned value of a dynamically dispatched object, that has a fixed size? With a box! Let’s rewrite the example:

trait Enchantment {
    fn enchant(self: Box<Self>, character: &mut Character);
}

struct Frogify;

impl Enchantment for Frogify {
    fn enchant(self: Box<Self>, character: &mut Character) {
        character.strength = 1;
    }
}

struct Character {
    strength: u32,
}

fn random_enchantment() -> Box<dyn Enchantment> {
    // not very random, but could be!
    Box::new(Frogify {})
}

fn main() {
    let mut prince = Character { strength: 10 };
    let enchantment = random_enchantment();
    enchantment.enchant(&mut prince);
    println!("prince has strength {}", prince.strength);
}

Note that this requires us to always box Enchantments before calling the enchant method, but that was worth it in my case!

Hope it helps in some way! <3