7 ms·
As a C++ dev I really don't follow. I don't remember any times where I thought something like a Union would help much. Where have you found so much use for the
by Night_Thastus 3y ago
As a C++ dev I really don't follow. I don't remember any times where I thought something like a Union would help much.
Where have you found so much use for them?
- fnfjfk 3y agoOption/Optional/Maybe is the most obvious one, as a basic example have you interacted with a API that sends some JSON where a field can be null or not? How would you represent that and distinguish it from a field that is always a string?
- Thaxll 3y agoNot many options, pointers most likely.
- TillE 3y agoThat's just std::optional<> vs std::string, isn't it?
- fnfjfk 3y agoYes, and std::optional is a janky implementation of a sum type in a language that doesn’t have them. The ergonomics are bad, and it is easy to make your program blow up if you forget a check. In a language with sum types like Rust, Kotlin, and Swift, if you’re following the default “happy path” of coding, you are not allowed to forget a check. The compiler will not let you.
- chriskrycho 3y agoThey're extremely useful in capturing the "business rules" for a given domain, particularly when it's a restricted domain: if something can only be `A | B | C`, being able to make that explicit, have it checked exhaustively everywhere by the compiler, and be able to dispatch on it easily using `match` is really powerful. That's because it comes with a statically-tracked "tag" (so: sometimes called a "tagged union") that the compiler can use to check you. You can write the same thing with a `union` in C or C++, with an `enum` to tag it, but you're doing that by hand rather than having it built in. Also: Where a struct or class's relationship over its members is the product of all of the types which make it up, a tagged union/enum is the sum of them. A common example: instead of a bunch of optional fields on a single class to model the different possible combinations, modeling something as only the legal set of combinations of fields. I recommend Scott Wlaschin's 'designing with types' series (uses F♯, but applies to any languages with 'sum types') for a great intro and then deep dive on the ideas here: https://fsharpforfunandprofit.com/series/designing-with-types/ https://fsharpforfunandprofit.com/series/designing-with-type...
- thrtythreeforty 3y agoI'm pretty fluent in C++ and Rust. You don't find much use for them in C++ because they're a relative pain in the ass without language support. std::variant is a sum type, but when's the last time you used it? I bet you had lots of fun with visitors and explicit type checks. The most motivating example is Rust's Result<T, E>. Either you have a "good" type T, or some error E occurred. Want to bubble this error up? Just do `my_result?` to get the underlying T or return E early. This idiom is pervasive and makes error handling via return codes very easy to use. It also makes it feasible to have types that have no "default" value. In C++ you have std::unique_ptr<T> which, for various reasons, has to have a nullptr state, or else it's frustrating to use. In Rust, owned pointers Box<T> don't have an empty state. If you need a nullable pointer, you use Option<Box<T>>, which is either Some(my_box) or None. The fact that Box<T> is always pointing to a valid object is a stronger guarantee than C++ smart pointers, and is only ergonomically possible because of the Option<T> sum type.
- gpderetta 3y agooptional<non_null_unique_ptr<T>> would be completely feasible in C++. It is just that std::optional and unique_ptr do not have the right semantics out of the box, but it is not hard to roll your own. But yes, I wish C++ has good builtin sum types and proper destructuring via pattern patching. Maybe one day....
- CodesInChaos 3y agoThe problem with adding a non_null_unique_ptr<T> to C++ is that moving needs to leave behind a valid value.
- gpderetta 3y agoYou are right, but you could make it not movable (and add destructive mode hooks for optional and containers). It is still possible to return non-movable, non-copyable types from functions. But I guess ergonomics would not be good.
- soulbadguy 3y ago
- diarrhea 3y agoA Union in itself is not a sum type. Rust even has unions: https://doc.rust-lang.org/reference/items/unions.html https://doc.rust-lang.org/reference/items/unions.html The unions of Rust are tagged unions that can hold data: https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html which is immensely powerful in data modeling.
- flohofwoe 3y agoThe main difference comes with the syntax sugar that's built into the language (and the additional information the compiler has). Sum types are also quite nice to bridge the gap from a 'dynamically typed mindset' into a 'statically typed mindset'. Sum types basically provide the ability that a variable can assume different types during its lifetime (? although I don't know if such 'shape-shifting' is allowed in Rust), so it almost feels like Python or Javascript, but since the compiler 'knows' sum types it's all still completely typesafe. In Typescript it's convenient and frankly quite nice, in a programming language like Rust where performance matters I'd be less enthusiastic though.
- ReactiveJelly 3y ago> I don't know if such shape-shifting is allowed in Rust I think so! You mean this? let mut x: Result <bool, String> = Ok (false); dbg! (&x); // Get a reference to the bool and mutate it if let Ok (b) = &mut x { *b = true; } dbg! (&x); // "shape-shift" to the other enum variant x = Err ("No boolean :(".to_string ()); dbg! (&x); It runs in Godbolt https://godbolt.org/z/q6jqYdYh1 https://godbolt.org/z/q6jqYdYh1
- jackmott42 3y agoYou can make a tagged union in any language, and that can often be useful, but when it is a standard part of the language you prevent any accidental cases of: 1. failing to check for sigils, because nobody uses a sigil when you have sum types. This would have prevented a famous exploit in SUDO a couple years back where someone forgot to check for the special negative value on an input parameter. 2. Eliminating null pointer exceptions or anything similar. You can't forget to check, you can still choose to have this kind of error, but you have to do it explicitly. Consider something like a function to open and give you back a file handle. In many languages such a function will return a pointer to a file, or null if there is a failure, or perhaps throw an exception if there is a failure. If you neglect to check for null, or forget to handle the exception, you have a problem. With sum types, the natural thing is to return something like a Result type, which is EITHER a file, or an error. And you have to check which it is, to get the File out.
- kouteiheika 3y agoThis is one of these things that you don't know you're missing until you actually start actively using. Back in the day when I was writing a lot of C++ I was saying the same thing as you.... until I wrote a significant program in Haskell, where sum types are also very common. And once it clicks it's hard to go back.
- ReactiveJelly 3y agoI flipped through one of my Rust projects and the best use case I saw was for serializing messages for disk / network / IPC: // This derive macro expands to a visitor pattern during build #[derive (serde::Serialize)] enum Message { QueryFoo (QueryFoo), MutateBar (MutateBar), } struct QueryFoo { foo_id: i64, } // If we receive a Message::MutateBar from outside our process, // it's a compile error for us to accidentally access the field `QueryFoo::foo_id`. struct MutateBar { bar_id: String, baz_value: u32, } fn send_msg_as_json (tx: &mut Sender, msg: &Message) -> anyhow::Result <()> { // The JSON serialization / deserialization is generated, too tx.send (serde_json::to_string (msg))?; Ok (()) } It's possible to represent this in C++, and you can integrate FlatBuffers or whatever to auto-generate the ser/de code, but then you're not writing C++, you're writing some weird DSL and you're fighting the build system and you're teaching juniors how to install it. I haven't seen anything in C++ that's both as easy and as airtight as a Rust enum with serde's automatic serialization macro on the top.
- gpderetta 3y agoFWIW using std::variant (or whatever is your in-house variant) to store message types is very common. Also in well designed low level binary protocols message types have a discriminant header already so often common to wrap them in an ad hoc-variant which doesn't have a separate discriminant.
- retrac 3y agoWe do not use unions very often in C/C++ because they are not safe. You have to know how to interpret it; the compiler can't and won't save you if you read the memory as the wrong type. You usually keep a variable set to the type the union is, and check it at run time. Some languages do that wrapping and checking for you, automatically. Everything from Ada (variant records) to Haskell has them. You cannot mishandle such objects by misinterpreting them as the wrong type; it's a compile-time error in those languages to do so. Imagine you are parsing something. In my opinion, the most natural way to express this, is a function that takes an array of characters and returns an array of parsed objects or tokens, where each object may be of a different possible type. In C, you'd probably use a union of structs, and set which type of struct as you parse each object. This is error prone and verbose, and the compiler can't double-check it for you. It's all automatic in a language with sum types. Quite efficiently too; most of the runtime checks can be optimized away.
- lostdog 3y agoTry representing JSON in C++. Sure, you can figure out a way that works, but the C++ code for printing out the JSON is much uglier than a language with tagged unions.
- pjmlp 3y agoThat is a consequence of lack of reflection, not sum types.
- ansgri 3y agosum types can often eliminate the need for reflection, and can have lower runtime cost.
- pjmlp 3y agoCompile time reflection doesn't have runtime cost.
- lostdog 3y agoCompile time reflection doesn't work for this case.
- pjmlp 3y agoTemplate metaprogramming techniques as poor man's reflection prove otherwise. Additionally that is one of the examples on the ongoing C++ reflection papers. Then there are a couple of examples in D, Common Lisp, Scheme, C# code generators.
- lostdog 3y agoNo? A JSON object could have any type at run time. You cannot do anything at compile time to figure out the right type. Please point to a specific library if you disagree.