Reputation:
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?
Upvotes: 2
Views: 1227
Reputation: 27885
You can make the original code compile by implementing A
for &'_ dyn A
and adding an explicit cast:
self.get(name).map(|borrow| Box::new(borrow.borrow()) as Box<dyn A>)
A closure is not a coercion site. The compiler looks at the contents of the closure to see what the return value is, and concludes that it returns Box<&'a dyn A>
. But the closure itself cannot be coerced from "function returning Box<&'a dyn A>
" to "function returning Box<dyn A + 'a>
", because those types are structurally different. You add the cast to tell the compiler that you wanted the closure to return Box<dyn A>
in the first place.
But this is a bit silly. Box
ing a reference is completely unnecessary here, and casting it to Box<dyn A>
just adds another level of indirection for the caller. It would be better to return a type that encapsulates the idea of "either a boxed trait object, or a reference to a trait object", as Peter Hall's answer describes.
In a future version of Rust, with generic associated types ("GATs"), it will be possible to make the return type an associated type of ProducerOrContainer
, something like the following:
trait ProducerOrContainer {
type Result<'a>: A;
fn get_a<'a>(&'a self, name: &'a str) -> Option<Result<'a>>;
}
With this trait definition, each type that implements ProducerOrContainer
can choose what type it returns, so you can pick Box<dyn A>
for some impl
s and &'a dyn A
for others. However, this is not possible in current Rust (1.29).
Upvotes: 3
Reputation: 58695
...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()))
}
}
Upvotes: 4