11 ms·
Std::visit is everything wrong with modern C++
- bluejekyll 9y ago> How do you teach this without overwhelming a beginner with all this other… stuff? > Is it expected to be common knowledge for your everyday programmer? > (And if the goal of adding variant to the standard library isn’t to make it a tool for the masses, shouldn’t it be?) These are great questions for any piece of software. Especially features being introduced in a language.
- alphaalpha101 9y agoYou don't 'teach it'. You learn it.
- pas 9y agoWhy would you teach someone C++, a language that has a lot of problems because it started as C with some sugar on top and then against all odds went ahead and added a ton of things that are very un-C-ish while still supporting all the C-ishness it had (has). Teach new languages to new programmers. Then when they have firm grasp on these concepts, and for some cruel trick of fate they have to do C++ development, then they can look this up.
- trendia 9y ago> The fact that we still handle dependencies in 2017 by literally copy-pasting files into each other with #include macros is obscene. Historically, C++ used includes so that it could be compatible with C. In the future, modules can be used which avoid many of the problems with includes [0]. [0] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4681.pdf http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n468...
- harpocrates 9y agoIs this implemented yet? The paper you linked is a draft. I'm glad C++ is doing this. > Historically, C++ used includes so that it could be compatible with C. I think it is more the case that C++ slowly splintered off C and never broke free completely of #include. Rust is also quite compatible with C without supporting anything like #include. It even has a module system!
- boris 9y agoIt is being implemented by the big three (GCC, Clang, and VC). In Clang and VC it is already usable to some extent.
- CyberDildonics 9y agoYou are talking about limited binary compatibility instead of source compatibility. These are two different things and can't be compared to one another.
- ChrisSD 9y agoSo the short version is that the C++ committee accepts a partial feature set despite key features not being ready in time? Perhaps they expect Boost to work round it until the next standard is released...
- blub 9y agostd::variant is fully featured, it's just that the convenient lambda-based make_visitor is missing.
- option_greek 9y agoHalf the way into the article, I felt like crying. Is it just me or is the standards committee actively trying to reduce the number of existing C++ programmers. I think we are better off with boost than learning this new stuff. I hope the people in standards committee will lose their C#/Java/<insert cool language> envy and be more selective in what they want to add to standards.
- jff 9y ago> I think we are better off with boost Whoa, let's not say things we can't take back ;)
- copx 9y agohttps://twitter.com/id_aa_carmack/status/81104943490146304 https://twitter.com/id_aa_carmack/status/81104943490146304
- xienze 9y agoOr you could just skip the part after the author went "ugh, I have to write a visitor, that's like, so many characters to type." He's whining over a fairly minor point and jumping through way more hoops than necessary. The proposed "default" way to visit std::variant is honestly pretty clear and simple in comparison to most C++ code.
- norswap 9y agoAt this point (and I suspect for a long time), the only thing C++ hasn't that Java and C# have is garbage collection.
- bitL 9y agoSeems like a design pattern from Java which often are badly converted functional programming snippets into OOP. I once loved C++, now I can't force myself to read the code anymore... Please add some monads there to destroy my hope for humanity once for all! I think maintainers of C++ have a case of "functional programming" envy.
- dfox 9y agoDiscriminated union is low-level pattern from non-OO languages (it is common pattern in C, Pascal has syntax for this and IIRC unions are always discriminated in "standard" Pascal) In object oriented languages this pattern is mostly unnecessary, because inheritance is usually better solution for the same problem. Only reason to use something like this in C++ is (maybe even only perceived) efficiency gained by removing level of pointer indirection.
- xyzzyz 9y agoThe sum type, which the author is talking about, is not the same as "discriminated union" pattern, the latter being the crude implementation of the former. It really comes from functional languages like ML, and is extremely useful and convenient when combined with pattern matching. In fact, attempts to emulate it in OOP using interfaces and visitor pattern tend to bring a lot of boilerplate, and obscure the actual logic, which is exactly what the author is complaining about. There's definitely more reason to use it than just "efficiency gain", which is why many languages introduced in the last decade have it built in (e.g. Rust, Scala or Swift).
- mcphage 9y ago> The sum type, which the author is talking about, is not the same as "discriminated union" pattern, the latter being the crude implementation of the former. Could you say something about the difference between the two? I'm not aware what it is. Edit: wow I accidentally a word.
- mrkgnao 9y ago
- JoshTriplett 9y agoI agree completely with everything in this article. But in addition, I think std::variant also misses the point of sum types in a big way. Sum types don't just store values of different types. They store different states, with associated data. So, for instance, consider the following simplistic expression AST; how would you store it in a std::variant? enum Expr { Number(usize), Negate(Expr), Add(Expr, Expr), Sub(Expr, Expr), Let(String, Expr, Expr), Var(String), }
- moomin 9y agoWell, the answer is you need a new type for every option in your list. That's not a massively satisfactory answer, but it is the answer...
- kraghen 9y agoThe C++17 equivalent would be something like the following (not tested): using NumberExpr = int; using VarExpr = std::string; struct AddExpr; using Expr = std::variant<NumberExpr, AddExpr, VarExpr>; struct AddExpr { std::unique_ptr<Expr> a; std::unique_ptr<Expr> b; } Of course, this being C++, you need forward declarations and a firm grasp of the rules of incomplete types to be confident about declaring a simple AST type.
- tomjakubowski 9y agoTo completely address JoshTriplett's point, yes, you can just define another struct for the SubExpr variant to disambiguate it from the AddExpr case. Requiring this kind of wrapping is awkward compared to e.g. Rust or Haskell's treatment of sum types, which unlike C++17 and std::visit both have powerful pattern matching features built into the language. Saying this as someone who writes C++ all day: std::visit and std::variant are weaksauce.
- deleted 9y ago[deleted]
- twic 9y ago
- clishem 9y agoThis blog post is not to be taken seriously in my opinion. For one thing, `printf` is obsolete in C++. This is how you should output values in a tagged union: std::variant<size_t, std::string> var = "test"; std::visit([](auto &&val) { std::cout << val; }, var); If you also want to print the name of the type of the variable, along with the value itself, well, C++ doesn't have reflection (yet). So you're going to have to write a function that takes a value and returns its type as a string, which you can already do using the typeid operator. This is basically implementing reflection yourself, it is cumberstone but doesn't require advanced template programming. There no need for any of the madness with explicit types inside the visitor lambda, which is deemed as neccesary by the author. Edit: here's another way to map types to strings. So no templates necessary at all for this entire problem. std::map<std::type_index, std::string> typeIdxToString; typeIdxToString[typeid(std::string)] = "string"; typeIdxToString[typeid(int)] = "int"; // ... Edit2: Turns out C++ has some form of reflection after all. You can just use: typeid(val).name() If your compiler supports pretty names.
- dleslie 9y agoHow much stack space does that use, are there allocations and how many are there and how are they managed? These questions seem utterly opaque to me with modern C++. Perhaps I just need to spend a weekend reading the docs again?
- jcelerier 9y ago> sizeof(std::variant<size_t, std::string>) == sizeof(size_t) + sizeof(std::string) + sizeof(discriminant) (at most 4 or 8 I'd guess?) std::variant will never allocate by itself.
- saghm 9y agoShouldn't it be `max(sizeof(size_t), sizeof(std::string)) + sizeof(discriminant)`? At least in other languages, that's how sum types work
- 9y ago
- AlexCoventry 9y agoThe struct solution seems OK to me, and the alternatives don't seem to accomplish much, brevity-wise. What's the problem with it?
- xwvvvvwx 9y agoYou would need the lambdas if you wanted to capture state from outside the visitor. But I agree, the struct seemed fine (not amazing, but good enough) to me.
- humanrebar 9y agoThe struct solution is a design pattern for creating a sum type. The lambda solution is a factory for creating any sum type as long as the parameter in each lambda is unique. Among other things, it's a design with weaker coupling.
- krona 9y agoI don't know why the author is so disparaging of if constexpr; I think it's going to push c++ metaprogramming in a direction that makes the more powerful aspects of the language easier to understand for beginners. Surely a good thing.
- jasonmaydie 9y agoIs the author complaining that the process is too complicated? This is typical of the c++ world, other languages make it easy but c++ isn't other languages.
- ausjke 9y agothe new c++ is great in that it is evolving fast, but I feel it is extremely hard to read, the readability of the language is diminishing by each newer version.
- PopsiclePete 9y agostd::<I<std::?<dont[know=what]>>(you) << mean >> std::it<looks,ok,to>(?auto=me)
- mark-r 9y agoI've been programming C++ for 20 years, and since C++11 the only feature I've seen that's worth the cognitive load is auto. Everything else just seems overly complicated. When I compare it to how easy things are in Python, I cry.
- umanwizard 9y agowhat about shared_ptr and unique_ptr ?
- mark-r 9y agoSorry, I misremembered the timeline. I thought they were part of C++03, but they're actually C++11. I was using shared_ptr when it was still part of Boost. I've edited my original comment to fix the error.
- umanwizard 9y agoBut `auto` was introduced in c++11, so your comment is still inaccurate ;)
- coldtea 9y agoParent said "since C++11 the only feature I've seen that's worth the cognitive load is auto". This means that (ignoring the mis-dating of the unique/shared_ptr) he already admitted, his comment is otherwise accurate.
- umanwizard 9y agoMy point was that `auto` was introduced in C++11, not "since C++11".
- coldtea 9y agoI don't think the parent's "since C++11" means "after C++11" but, "from C++11 and onwards". He explicitly said his reference was C++03
- amluto 9y agoI had a perfectly functional lambda-based visitor working in C++11. I don't see the problem. I can try to dig it up.
- any1 9y agoSum types can actually be implemented quite effectively using X-macros in both C and C++. In fact, I feel like they're simpler and more intuitive than this variant stuff. Edit: Let's use the same example. #define SETTINGS \ X(string, str) \ X(int, num) \ X(bool, b) struct Setting { union { #define X(type, name) type name; SETTINGS #undef X }; enum Type { #define X(type, ...) t_ ## type, SETTINGS #undef X }; Type tag; }; Printing settings like in the example becomes this: void printSettings(const Setting& s) { switch(s.tag) { case t_string: printf("A string: %s\n", s.str.c_str()); break; case t_int: printf("An integer: %d\n", s.num); break; case t_bool: printf("A boolean: %d\n", s.b); break; } } We can also load more things into the x-macro, so it's possible to define the switch cases above just like in the structure definition. We could add a third parameter called full_name: void printSettings(const Setting& s) { switch(s.tag) { #define X(type, name, full_name) \ case t_ ## type: std::cout << "A " full_name ":" << s.name << "\n"; #undef X } } Disclaimer: I have not compiled or run any of this code.
- jcelerier 9y agothere's absolutely zero compile time safety in your code. If someone copy-pastes for instance case t_int: printf("An integer: %d\n", s.num); break; case t_bool: printf("An boolean: %d\n", s.num); break; it will just break at runtime, unlike std::variant which prevents this at compile time
- any1 9y agoYes, which is why I would always use the second example for the print function. Edit: In fact it is possible to handle the cases explicitly while enforcing types by having the X-macros call functions in the switch/case and declaring prototypes via the macros: #define X(type, ...) void printSettings_ ## name(type); SETTINGS #undef X void printSettings(const Setting& s) { switch(s.tag) { #define X(type, name) \ case t_ ## type: printSettings_ ## name(s.name); break; SETTINGS #undef X } } Then you define printSettings_... and have them do stuff with the data.
- slavik81 9y agoSo, if you really hate making a struct each time, you drop make_visitor into the project's util.h and never worry about it again. I don't see the big deal.
- chombier 9y agoBesides, actually making a struct everytime is not that bad in practice. I also understand (and strongly support) the committee's desire not to introduce yet another language construct when a library solution can be worked out, given the horrible beast c++[11,14,17,20] has already become.
- Animats 9y ago"Sum types". Hey, let's give an old concept a new name. Those are called discriminated variant types, and they first appeared in Pascal. They're a straightforward concept, and can be implemented easily at the language level. The big problem with C++ is that the template fanatics took over. Templates are a crappy programming language - bad syntax, confusing semantics, and tough debugging. But there's no way to stop people from extending the language via templates. Hence Boost, and "you are not supposed to understand this" templates. (I fear that Rust is going down the same rathole.)
- jnbiche 9y ago> "Sum types". Hey, let's give an old concept a new name. Sum type has been the standard functional programming language term for this concept since at least the 1970s, perhaps earlier.
- maxlybbert 9y agoFrom the article: > it’s completely bonkers to expect the average user to build an overloaded callable object with recursive templates just to see if the thing they’re looking at holds an int or a string. You don't have to: http://en.cppreference.com/w/cpp/utility/variant/holds_alternative http://en.cppreference.com/w/cpp/utility/variant/holds_alter... (and http://en.cppreference.com/w/cpp/utility/variant/get http://en.cppreference.com/w/cpp/utility/variant/get to access the value).
- clishem 9y agoShhh... You're disturbing Hacker News' anti-C++ circle jerk.
- dang 9y agoPlease don't post snarky, unsubstantive comments regardless of how wrong other people are about programming languages. https://news.ycombinator.com/newsguidelines.html https://news.ycombinator.com/newsguidelines.html
- masklinn 9y agoAnd now you get neither exhaustive checking nor type-safe unwrapping, at this point is there really a point to variants? You may as well be using the old enum+union. (get_if at least nets you type-safe unwrapping similar to `if let` in Swift or Rust, though it returns a pointer rather than a reference)
- vinkelhake 9y agoYou either want exhaustive checking or "just [want] to see if the thing they’re looking at holds an int or a string". OPs comment was about the latter and holds_alternative and get accomplishes that. FWIW, std::get is type-safe in that you cannot specify a type outside of the variant types. It's safe at runtime in that it will throw std::bad_variant_access if the active object doesn't match the type. And because of this, you think we may as well use enum+union? Even if you only plan on manually type switching on a variant, std::variant saves you from a lot of boilerplate.
- lsh123 9y agoI might be missing something big but the "match (theSetting) {...}" example can be mapped to C++ as: switch(setting->tag) { case Str: // do something with setting->str break; case Int: // do something with setting->n break; case Bool: // do something with setting->b break; }
- hoytech 9y agoOne thing that a match method can provide is a compile-time check that you haven't forgotten to handle any cases of your variant.
- snowAbstraction 9y agoThe Clang C++ compiler will warn if the switch is not exhaustive.
- lsh123 9y agogcc will warn too
- humanrebar 9y agoFrom the article: """ Here be dragons, though, since we must always remember to: * Update tag whenever assigning a new value. * Only retrieve the correct type from the union (according to tag). * Call constructors and destructors at appropriate times for all non-trivial types. (string is the only one here, but you could imagine similar scenarios with others.) """ ...the last paragraph is the part that will almost surely trip a C++ newcomer up if they tried this out. The first to are (valid in my experience) complaints about maintenance mistakes that result in compiling programs but runtime errors.
- hoytech 9y agoI looked into this recently, and this is the implementation I ended up choosing: https://github.com/mapbox/variant https://github.com/mapbox/variant It has a very handy "match" method that addresses the issue raised in this blog post. They've also compiled many links to other variant implementations, as well as a bibliography on the standardisation efforts: https://github.com/mapbox/variant/blob/master/doc/other_implementations.md https://github.com/mapbox/variant/blob/master/doc/other_impl... https://github.com/mapbox/variant/blob/master/doc/standards_effort.md https://github.com/mapbox/variant/blob/master/doc/standards_...
- tlb 9y agoYou can use variants just fine without a visitor. Like, std::variant<string, int, bool> v; ... if (auto pstr = std::get_if<string>(&v)) { cout << *pstr << endl; } else if (auto pint = std::get_if<int>(&v)) { cout << *pint << endl; } else if (auto pbool = std::get_if<bool>(&v)) { cout << *pbool << endl; } else { count << "null" << endl; } which is not much worse than the pattern matching syntax.
- ezyang 9y agoYou don't get exhaustiveness checking, which is the killer feature of variants.
- kxyvr 9y agoFunny enough, std::visit was one of the features of c++17 that I was looking forward most to. Something that I don't entirely understand, though, and the author brings up is the lack of a function like make_visitor. Does anyone understand why or was there a late addition of a function that I'm not aware of? And, yes, std::visit is nonideal, but I do think it to be a much better option to using double dispatch and the visitor pattern, which is what we had to do before. More generally, I write numerical software and I do with there was a better option for writing this software outside of C++, but I don't see one right now. Specifically, C++ gives us direct access to the c-api of other languages and some pretty powerful tools to handle that. As such, if we want our software to work across multiple languages like Python, MATLAB/Octave, or a variety of other languages, C++ appears to be the best fit. Yes, it's possible to hook something like a Python code to MATLAB/Octave, but it's hard because Python and MATLAB/Octave handle memory in different ways. For example, they differ on how and when objects are collected by the garbage collector, so it makes it hard to use a Python object directly in MATLAB/Octave. In C++, we have enough tools to handle hooking C++ objects and items to other languages. Certainly, it's a pain, but I contend it's easier than to hook two other languages together through the c-api, but I find this more difficult to manage and we now have a bunch of additional code to maintain as well. As such, as many disadvantages as the language has, I appreciate new features like std::visit because it means that I can write easier code for my algorithms and still be able to hook to others.
- shpongled 9y agoI don't want to sound like a Rust evangelist... but it might be worth looking into. You get nice C FFI + some really nice features.
- kxyvr 9y agoActually, that's an interesting thought. I just did a quick check of the FFI and it looks promising. Thanks for the the idea!
- gpderetta 9y ago> Something that I don't entirely understand, though, and the author brings up is the lack of a function like make_visitor. Finite amount of committee time. It is better to ship working pieces and add missing pieces later than wait forever for the perfect solution. std::variant had been in bike-shed-mode for a very long time due to the never-empty-guarantee saga. At soon as some sort of consensus was reached, it was decided decided to ship what was ready.
- makecheck 9y agoNo doubt that std::visit appears to be a mess but why not call std::get<a_type>(a_variant) and trap bad_variant_access exceptions? It would still be a bit annoying to exception-wrap each attempt but it seems cleaner than any of the std::visit alternatives.
- MontagFTB 9y agoUsing exceptions as flow control is an anti-pattern in many modern C++ developer circles. It would also be excessively expensive.
- iammyIP 9y agoC++ is a meta language based on C. You can pick the features you want to use.
- lomnakkus 9y agoI think all the answers will probably come when C++17 support is good enough. See the example at [1], in particular the "overloaded" bit. I think my current GCC 7.2 should support it, but I don't think my current Clang 4.0.1 does. EDIT: Just tested and I can confirm that. [1] http://en.cppreference.com/w/cpp/utility/variant/visit#Example http://en.cppreference.com/w/cpp/utility/variant/visit#Examp...
- int_19h 9y agoThe reason why we have std::visit the way it is, is because that's the way it worked in Boost Variant, which is the basis for the proposal. The reason why it's the basis, is because it's a time-tested, proven and stable solution that has been around since 2002. The reason why it's so ugly, is because that's the best you could do in C++ back in 2002. So, there's a perfectly rational explanation for all this - it's not "insane". It is unfortunate that they didn't come up with a better API that would make use of new language features, but it's not like someone deliberately set down to design the more convoluted older API just to confuse people.
- kelvin0 9y agoJavascript and other dynamic languages are leaning towards type safety and other static checking tools. C++ and other compiled languages have strong types , but need and std::variant (and similar constructs) to add flexibility and boost productivity. The Yin and the Yang, looking to balance themselves into perfect programmer bliss.
- Davidbrcz 9y agoQuick survey std::variant<std::string,bool> v {"abc"}; Q: Which type is used ? (Since I'm asking a question, you know there is a gotcha). Answer: "abc" is const char*, which can be converted to bool and is picked because the way variant is designed.