Like I said, I was shocked but I was building something recently and, like you, reached for std::variant first to make the solution more generic while, in theory, outperforming type erasure/inheritance. Thankfully I had thorough micro benchmarks and was able to demonstrate convincingly that type erasure was drastically faster in practice, despite the theory (and virtual inheritance was better than both)..
I think what ends up happening is that std::variant and how you access it is pretty code bloaty and hasn’t been optimized yet properly by library authors and compilers. It’s also not particularly ergonomic so even though it’s been around for 3 years there’s not really any incentive (it’s slow and unergonomic, slow adoption, not the highest area for focusing on improving ergonomics and performance).
I think type erasure is overly criticized on how bad it performs. A single indirection in a hot loop is actually extremely CPU friendly as it’s predictor will keep the pipeline filled and you’ll be unlikely to end up flushing the pipe. It’s something the CPU has been optimized for since polymorphism is present in one way or another in all paradigms. Equally I suspect vtable costs are denigrated for no good reason. Like the cost is there (and you can accidentally overuse it) but the vtable is information to the compiler (+ it wouldn’t surprise me if CPUs had special tricks to make c++ vtables even faster) that can result in faster perf than you could achieve any other way. Again. I’m not talking about the theory here. I’m talking about the actual performance I wrote thorough microbenchmarks on.
The amount of code that gets generated to do discriminated std::variant unions is absurd which I suspect is a large part of the problem. I’m impressed that the standards body managed to pull off being so bad on so many dimensions. I’m not saying it’s a bad library. It has very specific requirements it’s trying to maintain specific to how the STL is written. Abseil’s HashTable reimplementation (SwissTable) should show how many of the invariants the STL likes to enforce in practice aren’t invariants anyone cares about and inhibit good performance.
Said another way. std::variant is probably the most efficient discriminated union you could get past the standards body to be included in the STL. Rust’s performs way better on any metric and that should give you a glimpse that the incentives in the standards body are misaligned with what’s actually needed to keep C++ healthy. I think - I haven’t benchmarked but tagged unions are so embedded in the language and type system rather than as “dumb” library code I can’t imagine they have the inefficiency baggage of std::variant.