Though not the original commenter, I would say it does not in my experience. The cost of using std::variant is much larger than that of Rust’s enum, even in the “unwound non-recursive” approach that is used by some implementations to support it’s storage.
The biggest cost factor is how visitation works. In Rust, you use match and can rely on the pattern matching to execute in a more or less safe switch. In C++, while you can use the .index() member function with a switch statement, you’ll typically have to manually keep track of each entry in a given variant.
At this point it’s no different than having a std::variant and then using an enum to keep track of the type so it’s easier to understand what’s going to execute (and then you need to manually extract the type, and since we don’t offer an “unsafe_get” to extract the underlying value from a variant without doing a bounds check, you’ll be doing a secondary check anyhow). If anything it’s quicker and easier to do an if-else chain with get_if, but that also has it’s costs as it’s also doing a bounds check on the index in every case.
Basically it is easier and more efficient for runtime operations to implement each variant-like type directly with an enum class and a proper type that returns said enum class from it’s index and then worry about managing the storage/construction for each individual type manually, possibly providing a match member function that can unroll any checks and be faster than std::visit.
That said if you don’t care about performance, go hog wild with std::variant. Sometimes it’s on a cold path or you can worry about the performance details later. 🙂
There is/was a paper to add an lvariant type to C++ to represent this concept (Authored by David Sankel), though some people in the room felt that enum union would make more sense, but it has since petered out.
Nice and terse write-up.
The examples were short and nice too.
A few passing thoughts on C++:
The fact that one could do MyEnumType { n } with any arbitrary integer in C++17, without any casting, bugs me.
To go with C++20’s std::underlying_to, is there a builtin that returns a type to be used on the RHS? Like
underlying_type<Color> red = to_underlying(Color::red); // type of red is `int32_t`.
For anyone on C++ version under 20 looking to stringify arbitrary enum types, check out the magic_enum library 1.
Every C++ standard feels like a lot of gunk being added to the language to make up for past mistakes, but C++26’s reflection syntax is especially egregious. Not that I could do better than the proposal.
I haven’t written C++ in quite a while.
I see this example:
int main() {
ComputeStatus s = ComputeStatus::Ok;
switch (s) {
case ComputeStatus::Ok:
std::cout << "ok"; break;
case ComputeStatus::Error:
std::cout << "Error"; break;
case ComputeStatus::FileError:
std::cout << "FileError"; break;
case ComputeStatus::NotEnoughMemory:
std::cout << "NotEnoughMemory"; break;
case ComputeStatus::TimeExceeded:
std::cout << "Time..."; break;
default: std::cout << "unknown...";
}
}
Why is the default case necessary? It would make sense for normal enums that can be converted from arbitrary integers, but the article specifically says that enum class cannot. Is C++ not capable of exhaustiveness checking?
it’s not necessary, and indeed adding it will suppress compiler warnings about an uncovered case if you ever expand the enum, which while not required afaik most compilers offer. (One could still forcibly cast an invalid integer value into the variable, but yeah, don’t do that…)
It would make sense for normal enums that can be converted from arbitrary integers, but the article specifically says that enum class cannot
It doesn’t say that, unless I’m missing something. About the closest I could find was:
In short, when you use enum class to define strong types, it’s helpful to allow initializing from the underlying type without any errors. This wasn’t possible before C++17.
… which actually is saying the opposite (look at the example), though the conversion must be explicit as it also notes.
As far as I know, it’s certainly possibly to have an enum class instance with a value that doesn’t match one of the declared members.
I’d really like for C++ to adopt Rust’s enum with internal data per variant, like:
Do you feel like std::variant doesn’t satisfy the algebraic data type/tagged union/sealed class goal?
Though not the original commenter, I would say it does not in my experience. The cost of using
std::variantis much larger than that of Rust’senum, even in the “unwound non-recursive” approach that is used by some implementations to support it’s storage.The biggest cost factor is how visitation works. In Rust, you use
matchand can rely on the pattern matching to execute in a more or less safe switch. In C++, while you can use the.index()member function with aswitchstatement, you’ll typically have to manually keep track of each entry in a given variant.At this point it’s no different than having a
std::variantand then using anenumto keep track of the type so it’s easier to understand what’s going to execute (and then you need to manually extract the type, and since we don’t offer an “unsafe_get” to extract the underlying value from a variant without doing a bounds check, you’ll be doing a secondary check anyhow). If anything it’s quicker and easier to do anif-elsechain withget_if, but that also has it’s costs as it’s also doing a bounds check on the index in every case.Basically it is easier and more efficient for runtime operations to implement each
variant-like type directly with anenum classand a proper type that returns saidenum classfrom it’sindexand then worry about managing the storage/construction for each individual type manually, possibly providing amatchmember function that can unroll any checks and be faster thanstd::visit.That said if you don’t care about performance, go hog wild with
std::variant. Sometimes it’s on a cold path or you can worry about the performance details later. 🙂There is/was a paper to add an
lvarianttype to C++ to represent this concept (Authored by David Sankel), though some people in the room felt thatenum unionwould make more sense, but it has since petered out.I find std::variant to be very awkward (Just like std::optional).
It shouldn’t be possible to call
get<n>()without checking if the variant being accessed is correct.I’ll have to check that out…
Nice and terse write-up. The examples were short and nice too.
A few passing thoughts on C++:
MyEnumType { n }with any arbitrary integer in C++17, without any casting, bugs me.std::underlying_to, is there a builtin that returns a type to be used on the RHS? LikeEDIT: Looks like there is
underlying_typeafter-all https://en.cppreference.com/w/cpp/types/underlying_typeI haven’t written C++ in quite a while. I see this example:
Why is the
defaultcase necessary? It would make sense for normalenums that can be converted from arbitrary integers, but the article specifically says thatenum classcannot. Is C++ not capable of exhaustiveness checking?it’s not necessary, and indeed adding it will suppress compiler warnings about an uncovered case if you ever expand the enum, which while not required afaik most compilers offer. (One could still forcibly cast an invalid integer value into the variable, but yeah, don’t do that…)
It doesn’t say that, unless I’m missing something. About the closest I could find was:
… which actually is saying the opposite (look at the example), though the conversion must be explicit as it also notes.
As far as I know, it’s certainly possibly to have an
enum classinstance with a value that doesn’t match one of the declared members.