Match-variants — C++ std::visit for Rust enums: static dispatch without the boilerplate

match-variants is an implementation of the C++ std::visit idea for Rust enums: preserve concrete types and static dispatch, while avoiding repetitive match boilerplate.

Rust gives us two common ways to work with different concrete types: dynamic dispatch through dyn Trait, or an enum with pattern matching.

With an enum:

enum Value {
    Foo(Foo),
    Bar(Bar),
    Baz(Baz),
}

after pattern matching, every branch operates on its concrete type:

match value {
    Value::Foo(v) => v.process(),
    Value::Bar(v) => v.process(),
    Value::Baz(v) => v.process(),
}

This allows Rust to retain static dispatch instead of calling through a trait-object vtable.

And the performance difference can be significant. enum_dispatch reports up to 10× faster performance than dynamic dispatch in its benchmarks.

So there is a good reason to use:

enum → pattern matching → concrete type → static dispatch

However, when we do this, we pay in boilerplate

For three variants, this isn't a big problem.

For an enum with 20 or 50 variants, it becomes a lot of repetitive code:

match value {
    Value::Foo(v) => process(v),
    Value::Bar(v) => process(v),
    Value::Baz(v) => process(v),
    // ... another 20 or 50 arms
}

Very often, these arms aren't expressing different logic at all.

Every arm is mechanically saying the same thing:

unwrap the variant, preserve its concrete type, and apply the same expression to it.

Then you add another operation and write essentially the same 20 or 50 match arms again.

This is exactly the boilerplate match-variants is intended to remove.

The generated code is still an ordinary exhaustive Rust match. Every arm still operates on its concrete type and retains static dispatch.

You just write the common operation once.

C++ std::visit

C++ already has a very nice abstraction for this problem.

Given a std::variant:

using Value = std::variant<Foo, Bar, Baz>;

you can write:

std::visit([](auto&& value) {
    value.process();
}, value);

The visitor receives the concrete alternative stored in the variant.

You don't manually write separate boilerplate for Foo, Bar, Baz, and every other alternative just to recover the concrete value.

The operation is written once and instantiated for the concrete alternatives.

That's the model behind match-variants: something similar to C++ std::visit([]) for Rust enums.

Keep the enum.
Keep the concrete types.
Keep static dispatch.
Remove tons of repetitive match boilerplate.

Why not enum_dispatch?

enum_dispatch is a great solution when all variants can be represented through a common trait.

In fact, its own benchmarks demonstrate one of the reasons for keeping enum-based static dispatch in the first place: it reports performance improvements of up to 10× over dynamic dispatch in its benchmarks.

But requiring a common trait isn't always the abstraction we need.

Sometimes we need to visit the concrete value itself and let an expression be type-checked independently for every variant.

This distinction becomes important with associated types, associated functions without self, concrete Self, and cases where there simply isn't an appropriate common trait.

I've documented the differences from enum_dispatch, with concrete examples, in the match-variants README:

https://crates.io/crates/match-variants

The goal of match-variants is simple:

bring the std::visit idea to Rust enums: write the operation once, preserve concrete types and static dispatch, and let generated exhaustive pattern matching handle the boilerplate.