Rust Enums in Modern C++
Lately I see rust and the C and C++ languages being compared a lot in performance and capabilities. A lot of people support rust for its helpful compiler messages and its safety. And that is all true. I do like rust for these reasons. I have come across a lot of people that comment about rust enums being really powerful and I wanted to showcase how one can achieve the same functionality in C++.
Table of Contents
Introduction
What are Rust enums? Enumeration values are when you want to have a type that can only take a few predefinded values. For example if we look at an IP there are two standards for IPs – IPv4 and IPv6. They are written in a different way and you might want to distinguish in your code whenever you’re dealing with one or the other. Since they are both string values you would have to introduce a second value that defines the type. This value would be IpKind
and it could hold values of IPv4
and IPv6
. In rust defining this type would be like this:
enum IpKind {
IPv4,
IPv6,
}
Simple right? Well this is pretty much the same as defining an enumaration in C++:
enum IpKind {
IPv4,
IPv6
};
It is literally the same. Where rust enums get a lot of hype though is having the ability to have values inside of them. So they actually become something like a struct. Rust will encapsulate this though and just give you access to the values inside.
enum IpAddr {
V4(String),
V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
This will allow you not only to instantiate a variable of that enum type but also store values in it like a struct. You could even go more advanced and name those variables. Like a struct is the key phrase here because this is essentially what rust will do. Those values will be stored in an union like structure with a type index that defines what the current enumeration type is.
C++’s approach to Rust Enums
In the latest C++ versions the language introduces the concept of a variant. You could #include <variant>
and you will be able to use the std::variant<>
template structure. This structure allows you to pass in multiple types and it will store an index that would keep track of which type is currently stored in the variant. Let’s takte the above example in mind and create an enum with three values – invalid, ipv4 and ipv6:
struct InvalidIP {};
struct IPv4 {
std::string value;
};
struct IPv6 {
std::string value;
};
using IPType = std::variant<InvalidIP, IPv4, IPv6>;
Then you could use the IPType to create variables and also test values:
IPType ipType = InvalidIP{};
if (IPv4 *ip = std::get_if<IPv4>(&ipType))
{
std::cout << ip->value << std::endl;
}
else if (IPv6 *ip = std::get_if<IPv6>(&ipType))
{
std::cout << ip->value << std::endl;
}
else if (InvalidIP *ip = std::get_if<InvalidIP>(&ipType))
{
std::cout << "Invalid IP" << std::endl;
}
So it is actually very easy to achieve the same functionality in C++ as it is in rust.
Conclusion
Rust enums are an amazing rust feature but it not a feature that you cannot find in other languages. It might not be integrated in C++ but it is very performant and usable to use std::variant.
Having a built-in coproduct/algebraic Union types makes Rust very hot. In fact the first thing about rust was this memory safety... it's nice but I got really fixed on rust when I realized that they have built-in algebraic Union types, pattern matching and the strong influence from functional programming (with C performance :)) Looking at the memory layout, a std::variant is very similar to rust - storing a type tag + a union value. However, C++ works different in "pattern matching"... There's no real pattern matching. But with std::visit, lambdas and overloads (see the example https://en.cppreference.com/w/cpp/utility/variant/visit) it's at least possible to "match" on types. With C++20 concepts and templates numbers there is even more freedom with this type of matching. You may want to add some examples with std::visit - cause that's the counterpart to pattern matching in rust. Without that mechanism variants are very useless IMO
The problem with the missing language support for std::variant is not only a less clean syntax. More complex use cases like nested variants are also much less efficient. If I understand it correctly, the C++ compiler can't just merge all the tags of the nested variants into one and do things like niche optimizations, because the variants are implemented as normal templates. This leads to a lot of space (and maybe time) overhead when using a lot of variants. Additionally, something like a match statement is needed for good ergonomics. Can you do switch case over variants?
You could compare how accessing specific enum looks like in Rust vs C++ (if-else). I didn't use Rust but in C++ using variant seems to be little bit clonk'y. I wish C++ had nicer syntax for that? Using std::visit introduces a lot of indention's.
Rust’s enums really shine in combination with match, which C++ doesn’t seem to have — but std::visit does let you use a very similar pattern with a bit of template magic! (See example 4 in the linked code.)
You should check out folly::variant_match :)
The only problem of this post is that it uses PascalCase haha. Just kidding! I was just trying to find a problem. Thanks! Keep it up. Greetings from Germany.
Leave a comment