We can add generic methods to otherwise object-safe traits by adding a Sized
requirement:
trait T {
fn spec(&self) -> u32;
fn gen<X: From<u32>>(&self) -> X where Self: Sized {
From::from(self.spec())
}
}
but calling generic functions from type-erased trait objects is not possible:
// okay
fn f(t: &T) -> u32 { t.spec() }
// error: the `gen` method cannot be invoked on a trait object
// fn g_(t: &T) -> u32 { t.gen() }
This is quite understandable (generic functions cannot be included in a vtable), but we can get around it:
impl<'a> T for &'a T {
fn spec(&self) -> u32 {
(*self).spec()
}
}
fn g(t: &T) -> u32 { (&t).gen() }
(also with impl T for Box<T> { ... }
).
Why does the above work? Because the trait-object &'a T
is a Sized
object, and the above impl
rule creates a vtable for it. But there’s one niggling issue: the need to use (&t).gen()
instead of t.gen()
.
Actually, we can get around that issue by making gen
consume self
instead of &self
, but this causes t.gen()
to consume t
:
trait T {
...
fn gen_consume<X: From<u32>>(self) -> X where Self: Sized {
From::from(self.spec())
}
}
fn h(t: &T) -> u32 { t.gen_consume() }
but calling gen_consume
on a type which is not already a reference (or supports Copy
) therefore consumes the object, unless manually referenced (e.g. (&o).gen_consume()
).
This approach is workable, except perhaps for one thing: the error message on g_
neither suggests the above impl
rule, nor is affected by it, so there is no hint that users can fix it simply by replacing t
with (&t)
. Since many users find traits bewildering and may not understand how dynamic dispatch works I consider this a significant drawback — is it fixable?
Link to the above on play.rust-lang.org
There are some alternatives: using an extension trait separates the object-safe and non-object-safe parts of the trait, and thereby allows use of generic methods on the type-erased parent trait:
trait XT: T + Sized {
fn gen<X: From<u32>>(&self) -> X {
From::from(self.spec())
}
fn gen_consume<X: From<u32>>(self) -> X {
From::from(self.spec())
}
}
impl<TT: T + Sized> XT for TT {}
fn g(t: &T) -> u32 { t.gen() }
What’s more, this has reasonable error messages if the extension trait is not in scope:
error[E0599]: no method named `gen` found for type `&T` in the current scope
--> src/main.rs:29:29
|
29 | fn g(t: &T) -> u32 { &t.gen() }
| ^^^
|
= help: items from traits can only be used if the trait is in scope
= note: the following trait is implemented but not in scope, perhaps add a `use` for it:
candidate #1: `use XT;`
Serde uses a similar trick on type-erased trait-objects, mainly different in that the generic trait does not inherit the object-safe trait.
I should also mention Huon Wilson’s blog post on allowing generic methods and object-safe traits.
So why write all this?
- These are tricky concepts to understand and it is not immediately obvious how or why the tricks work; hopefully this helps a few people understand.
- To ask: is there a way to make
t.gen::<u32>()
work fort: &T
without the extra trait? - Can the compiler at least suggest usage of
(&t).gen()
?