In terms of the work done to use it, are we better off fixing the polymorphization and devirtualization optimizations so that we don't need it?
Enum dispatch lies somewhere between monomorphized generics and dynamic dispatch.
If I have code like:
pub fn foo<D: Display>(item: D) { … }
then the compiler will generate one version of foo
for each type D
that it's called with - so there will be code for foo<String>
, foo<u32>
etc in the final binary. There's a trick to optimizing this, called "polymorphization", where the compiler knows that most of foo
's code will be identical between versions of foo
, and doesn't output as many copies - for lots of detail, look at what the Polymorphization Working Group is working on.
There's also a sometimes-applicable manual trick for optimizing this, where you have an inner function that's not generic, and a generic wrapper:
pub fn foo<P: AsRef<Path>>(path: P) {
fn foo_inner(path: &Path) { … }
foo_inner(path.as_ref())
}
This results in foo_inner
existing once, while foo
is still monomorphized many times, but is tiny before optimizations. The polymorphization WG would like to optimize functions like this automatically into this form, where possible.
At the other extreme, you have indirect dispatch via trait objects:
pub fn foo(item: &dyn Display) { … }
This is fully dynamic - item
has a vtable, and the compiler generates one body for foo
, plus an indirect jump through the vtable every time you use item
. Sometimes, the compiler can optimize this via something called "devirtualization", where it works out what possible values the vtable can take on, and removes the indirection.
Enum dispatch sits in the middle. You auto-generate (or hand-write) an enum that has one variant for each implementation of the trait that you want to use, and you implement the trait for that enum. So:
// All types in this enum implement `Display`, and its only API surface is
// the `impl Display` below
enum Displayable {
U32(u32),
String(String),
F64(f64),
IpAddr(IpAddr),
}
impl Display for Displayable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::U32(d) => d.fmt(f),
Self::String(d) => d.fmt(f),
Self::F64(d) => d.fmt(f),
Self::IpAddr(d) => d.fmt(f),
}
}
}
(you can see how this sort of code is easy to auto-generate).
Then, as long as the only implementations of Display
that you care about have a variant in Displayable
, you can take the enum instead of being generic (and monomorphized), or taking a trait object:
pub fn foo(item: Displayable) { … }
Personally, I'm not convinced that this should have special syntax - it feels like something the Polymorphization Working Group "should" implement as an optimized implementation of the generic form, where it makes sense - i.e. you always write:
pub fn foo<D: Display>(item: D) { … }
and the compiler works out for itself whether that's better implemented by monomorphization, or by generating "enum dispatch" type of code.