Search code examples
rusttrait-objects

"Expected trait A, found &A" when trying to box a trait object


I'm trying to make a trait that can either retrieve (and return a reference to) a trait object of another trait, or create one (and return a boxed version of it), leaving the choice to the implementor (which means I need to restrict the returned object's lifetime to that of the producer). However, I'm running into errors:

use std::borrow::Borrow;
use std::collections::HashMap;

trait A { 
    fn foobar(&self) {
        println!("!"); 
    } 
}

trait ProducerOrContainer {
    fn get_a<'a>(&'a self, name: &'a str) -> Option<Box<dyn A + 'a>>;
}

impl<'b, B: Borrow<A>> ProducerOrContainer for HashMap<&'b str, B> {
    fn get_a<'a>(&'a self, name: &'a str) -> Option<Box<dyn A + 'a>> {
        self.get(name).map(|borrow| Box::new(borrow.borrow()))
    }
}

The error is:

error[E0308]: mismatched types
  --> src/main.rs:20:9
   |
20 |         self.get(name).map(|borrow| Box::new(borrow.borrow()))
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected trait A, found &A
   |
   = note: expected type `std::option::Option<std::boxed::Box<dyn A + 'a>>`
              found type `std::option::Option<std::boxed::Box<&dyn A>>`

Which puzzles me, because I'd expect a &A to be an A too. I've tried to impl<'a> A for &'a A, but that doesn't help either. Is there any way to fix this?


Solution

  • ...that can either retrieve (and return a reference to) a trait object of another trait, or create one (and return a boxed version of it).

    With this requirement, a Box will not work. A Box owns its data, but you sometimes have borrowed data, which you can't move.

    There is a type in the standard library called Cow, which is an abstraction over whether a value is borrowed or owned. However, it may not be quite suitable for you here because it won't let you own the data as a Box and it also requires that your data type must implement ToOwned.

    But we can take your requirement and model it directly as an enum:

    enum BoxOrBorrow<'a, T: 'a + ?Sized> {
        Boxed(Box<T>),
        Borrowed(&'a T),
    }
    

    And make it ergonomic to use by implementing Deref:

    use std::ops::Deref;
    
    impl<'a, T> Deref for BoxOrBorrow<'a, T> {
        type Target = T;
        fn deref(&self) -> &T {
            match self {
                BoxOrBorrow::Boxed(b) => &b,
                BoxOrBorrow::Borrowed(b) => &b,
            }
        }
    }
    

    This lets you treat the custom BoxOrBorrow type as any other reference - you can dereference it with * or pass it to any function that expects a reference to T.

    This is what your code would look like:

    trait ProducerOrContainer {
        fn get_a<'a>(&'a self, name: &'a str) -> Option<BoxOrBorrow<'a, dyn A + 'a>>;
    }
    
    impl<'b, B: Borrow<dyn A>> ProducerOrContainer for HashMap<&'b str, B> {
        fn get_a<'a>(&'a self, name: &'a str) -> Option<BoxOrBorrow<'a, dyn A + 'a>> {
            self.get(name)
                .map(|b| BoxOrBorrow::Borrowed(b.borrow()))
        }
    }