Std::visit is everything wrong with modern C++ (2017)
bitbashing.io
bitbashing.io
Many are stuck on C++ for legacy reasons. I have worked in such large C++ legacy systems for many years in the past. The average programmer simply cannot deal effectively with this modern C++. It is too complex.
I’ve been down that road being exited about new C++ features only to realize that I’ve actually reduced the productivity of my co-workers.
Often I wonder if C++ programmers all suffer a Stockholm syndrome. They have come to sympathize with their hostage taker C++, making up excuses for the many ways C++ abuse and terrorize them.
I go to these C++ conference talks in occasion and I see people in ecstatic praise about how some genius C++ guru came up with an elaborate convoluted solution to something which is like two lines of code in a sane programming language.
Seriously I think C++ has drained so much of their brainpower that they have simply not been able to look outside and see how the grass is greener everywhere else.
Ironically, if one wants all of those things, the best alternative at the moment may be the C language.
That's crippling. Arguably the best use case for C++ is coding video games, and portability is huge there.
Of which Rust has backends for all them, even the web via wasm.
While Rust is not as portable, it's not like it's strictly limited to well functioning x86 boxes, it'll produce binaries for anything that LLVM supports, which is quite a lot by now.
Are those backends officially supported on consoles? Do all those consoles have libraries and tools (profilers, debuggers, etc) that support Rust?
We'll see if this changes in the future, especially with some of the names doing Rust dev in these places.
C++14 and C++17 were minor upgrades by comparison, so I'd say C++11 can rightly be called modern C++, though others may disagree.
Of course when it comes to practical portability today then you're right as there are plenty more C++ compilers for different platforms than Rust compilers (then again, there are more C compilers than C++ in that regard). This may eventually change over time, tho, but it sure is something to consider today.
PS and edit: I don't mind you disagreeing with me, but would anybody care to illuminate me as to what your disagreement is about?
https://github.com/conan-io/conan
https://github.com/microsoft/vcpkg
https://github.com/cpp-pm/hunter
Another option if you're on Linux/BSD is to use your native package manager. C++ packages are usually included in those repositories. You also may be able to install Brew, Nix, or Guix on any given distribution.
There goes the portability.
Stockholm Syndrome is more about convincing yourself that a bad situation is good because you're stuck in it. I don't think there are too many people "stuck" with Rust yet. On the contrary, I think the borrow checker is a draw.
As you identified yourself, that's not entirely true, because C offers all of those things. But the really tragic thing about the status quo is that because there is so much sunk effort behind the C and C++ ecosystem, the resulting momentum makes it difficult for new languages that are simply better to gain traction.
In a way, C++ is the ultimate demonstration of how important the surrounding tool, library and developer ecosystem is to the success (== usefulness in practice) of a programming language. If the language itself were the dominant factor, the writing would have been on the wall when almost every language at a higher level than C was adding features like first class functions and C++ was trying to do something vaguely similar with overcomplicated binder templates.
A language with nicer syntax for working with higher-order functions, Haskell say, might have used
xs = [1, 2, 3, 4, 5]
n = count_if (<3) xs
But for many years in C++, the closest theoretical equivalent would have been to write something like xs = // some standard container type, tediously constructed
n = count_if(xs.begin(), xs.end(), bind2nd(less<int>(), 3))
instead. Obviously hardly any real programmers ever did that, and it's true that modern C++ is better in several relevant ways, but it's been literally decades and it still hasn't entirely caught up.Meanwhile, several much more promising languages are struggling to break into the kinds of markets they deserve to because they haven't achieved a critical mass of support. And the world continues to suffer the loss of productivity and problems with security and reliability that come from using a language like C++ for things that do actually matter. It's an understandable situation, but still a regrettable one.
You're right, I contradicted myself there a bit. Fixed. But C is almost a subset of C++, except for some technicalities. So it's a bit hard to call it an actual alternative. Switching from C++ to C is more or less a matter of restricting oneself to less functionality (which in some cases is a good thing, but I digress).
Other than C it's hard to point at a solid alternative for many C++ use cases.
> In a way, C++ is the ultimate demonstration of how important the surrounding tool, library and developer ecosystem is to the success
Indeed! This is something that any language wanting to compete with C++ must get right. It's a hard thing to do.
I suspect it's actually an impossible thing for a new language to do on its own, because it's not really a technical problem in the first place. It needs a new language with a "killer feature" and serious resources backing it to break through. Several of the relatively success new(ish) languages have combined those two attributes, with the language being in some sense the favoured one for writing software that runs on a certain platform.
Gradually porting like this lets you keep using your code base whilst introducing new or overly complex stuff in a language that's faster and easier to write (usually ends up with less than half the LoC of the equivalent C++ but often way, way less than that). Metaprogramming is also very nice, easier to reason about and perhaps most importantly, even with lots of macros doesn't noticeably affect the fast compiles.
Another advantage is once you have some Nim code you can choose to change the target to C or ObjC (or even JS or LLVM) so you're actually increasing portability.
This all depends on how you rank 'maturity' of course. Nim's been around for longer than Rust and Go IIRC, and it's been rock solid for me but you may have different parameters. It certainly helps being able to directly use libraries for C and C++ if you can't find an appropriate Nim implementation.
Edit: For contrast, consider the challenges in the article for variants in C++, then the equivilent object variants in Nim:
type
MyVarKind = enum mvkNumber, mvkString
MyVariant = object
case kind: MyVarKind
of mvkNumber:
num: int
of mvkString:
str: string
var myVariant = MyVariant(kind: mvkNumber)
myVariant.num = 1
myVariant.str = "Oops" # Error: 'str' is not accessible using discriminant 'kind' of type 'MyVariant'A guide to debugging and profiling: https://nim-lang.org/blog/2017/10/02/documenting-profiling-a...
Guide for working directly with GDB-Nim: https://internet-of-tomohiro.netlify.app/nim/gdb.en.html
A walk through and extra info with the language devs: https://www.youtube.com/watch?v=DmYOPkI_LzU
However GDB shows the name mangling suffix in generated code, and types are their native types. There's a script called nim-gdb to add pretty printers for types to the GDB output to show the Nim source types.
Perhaps surprisingly though, the C and C++ generated output itself is fairly straightforward, even with name mangling suffixes. The inserted directives tell you the Nim source line so you can navigate it fairly well if you want to, and the suffix means the variable is unique referenced in the code. As far as I know you can use any debugger that supports the target language, though I've not tried anything but GDB myself.
It's very rare for me to dig into the generated code but sometimes I'm curious about the data structure analog in the target language. In the case of Nim's object variants, last time I looked when compiling to C they were ultimately reduced to simple checked union types.
It's worth mentioning that by default all types in Nim are stack allocated, and you have full manual memory management to the same level as C/C++ but with better type safety and less boilerplate. The GC is only used when you tag a type as `ref`, in strings, and the 'vector' dynamic list type, `seq`.
The newer GC, ARC (not related to Swift's ARC), is similar to RAII - scope based, non-atomic, deterministic, shares memory between threads but not stop-the-world, and uses move semantics: https://nim-lang.org/docs/destructors.html
This makes the GC a nice to use addition for resource management but not a fundamental requirement or speed limitation.
In my experience the default (thread-local refc + cycle collection) GC is very performant already, but it's straightforward to write code that works entirely on the stack, or create objects that wrap manual heap allocs, or use custom external memory allocators. Passing `--gc:none` removes the GC entirely from the compilation target, for example if you're working with very constrained embedded devices with the caveat that less of the stdlib is available (currently).
The ARC GC (doesn't handle cycles unlike it's sibling ORC) is aiming to be lean enough to be used in hard realtime and memory constrained embedded systems. For hard realtime though I'd expect most people would just manually manage their types anyway on the heap or stack.
If you're doing interop between Nim and C++ and want Nim's GC to manage types that you're passing directly to pure C++ code, you can tell the GC that the data is still being used with GC_Ref() or not with GC_Unref(). There are a few libraries for C/C++ interop, such as: https://github.com/nimterop/nimterop
Personally in this case I would probably just manually allocate memory memory in Nim or C++ and not use GC'd types across boundaries for clarity if nothing else, still it's an option if your design requires it.
Finally, there is a tool to help auto-translate C/C++ to Nim with the c2nim tool: https://github.com/nim-lang/c2nim and docs: https://github.com/nim-lang/c2nim/blob/master/doc/c2nim.rst
sounds like C# and Java with only performance being questionable, but Java's used in HFT, so it definitely can compete.
This is a bad argument and if you're using Java, you're not competing in the very speed-critical parts of HFT. Nor is Java common among HFT shops at all.
well-written C++ is much faster than Java.
So saying that
> Nor is Java common among HFT shops at all.
Sounds weird, especially when it appears among job postings.
I play around with new system languages by implementing parts of database engines (typically written in modern C++) in the new language. It terms of abstract code architecture, you can port a C-style database design with relative ease -- most new system languages aim to be an improved C with direct portability in mind -- but not modern C++-style database design, and the latter is unambiguously superior for performance and correctness when writing database engines. C++ will continue to see a lot of usage as long as it is difficult or impossible to write code in other languages that is functionally equivalent to C++.
In C++-style designs, you end up writing surprisingly little code, having metaprogramming scaffolding generate most of the code for you while doing fairly deep correctness and type safety checks at compile-time. Someone has to write the scaffolding libraries but they aren't that large, just tedious, and they get reused. Resource management, change detection, etc is automagic because C++ makes that easy to hide even in complex cases like DMA I/O. Every data structure and algorithm is highly optimized for the local use case and using the most highly compressed representation reasonable in context. It would be impractical to write all of this code and analyze it manually.
I used to write databases in C99. It required several times the lines of code relative to C++17, with worse results, even if you include the scaffolding libraries. C++17 implementations, done well, is much closer to writing a specification for a subsystem design and behavior and having the compiler generate an optimized implementation for that specification and exporting types that hide the fiddly details that can be safely composed with other generated types, than writing code. The scaffolding is also generic and flexible: some can generate an OLTP database engine just as easily as an OLAP database engine largely by changing the subsystem specifications and composing things differently. This would not be possible without heavy use of the metaprogramming facilities to both generate the types and guarantee that type interactions will still be safe and reasonably optimal.
One thing to understand is that these libraries are highly opinionated about the abstract architectural model. It doesn’t make a lot of sense to mix components from user space and kernel space designs, for example, though both have advantages separately. It tends to be more along the lines of one abstract architectural model and enormous amounts of elasticity and flexibility within that model based on the data models, workloads, transaction semantics, and hardware you are targeting. You also still have to write a spec that makes sense from a database engineering standpoint.
At least for me, there are still significant parts of a database engine for which I haven’t built a metaprogramming scaffold. That is largely a matter of time and effort. Other parts I haven’t had to write much code for years but still get state-of-the-art implementation to spec.
Ultimately, I’m trying to automate away my job.
Compare to writing a database system in something like Go. Sure you make end up with 50% more code, but you could have anybody up and running reading and understanding the code within 3 days.
IBM have done studies of this and found that fancy code is not all that valuable. It ends up falling in disuse over time as people don't get it. I have seen my fair share of C++ code which simply had to be tossed because nobody at the company could understand what the previous whiz kid had written.
You can’t write a comparable database engine in Go, fundamentally. The language lacks features required for competitive performance. The code difference will be much more than 50% trying to get the most out of what Go is capable of in this domain.
The point of writing code this way isn’t to be clever or for a “thrill”, it objectively produces superior performance, reliability, and maintainability. Defects scale with the number of lines of code regardless of language. Type safe code gen is a powerful tool.
What are the specific C++17 features that make this a reality?
For C++, the overarching criterion for almost any language feature is how useful it is for capturing semantics in an easy-to-use, performant, powerful, and generally usable library.
The consequence is that using new core language features in non-library code often seems unnecessarily complicated, but a library constructed using the feature as intended is insanely powerful.
The result is that you can write libraries in C++ you cannot write in other languages, and you can call into libraries that are so powerful only from C++.
In most other languages, using a library means you give something up: usually performance, and often it comes with restrictions. The standard of usefulness for C++ libraries is very high, and always increasing.
Something similar happened with coroutines, although what we did get has hooks that a library should be able to patch into, to achieve wonders not yet seen.
Switching to Swift I felt almost everything worked as it should. It was almost a bit boring not having to deal with all the usual crap that C++ would give me. I am willing to be you can write any C++ system much better in Swift. C++ would probably have a performance edge, but in 95% of cases not enough to be worth dealing with an ugly language like C++.
> C++ will continue to see a lot of usage as long as it is difficult or impossible to write code in other languages that is functionally equivalent to C++.
I would challenge you to give me any language construct that is indispensable for a particular type of program which cannot be done more elegantly in another language and with less headache.
I cannot think of a single feature in C++ which I have ever missed in any other of my preferred languages.
If you can’t think of reasons to use C++, that isn’t because reasons don’t exist.
It seems you're complaining about the outcome of poor and ill-advised engineering practices instead of a programming language.
And your description also covey's the idea that you had people who knew very little about C++ trying to figure out how to use it in ways that they have no idea was possible to use.
I get the appeal of a good scapegoat. Yet, from your description it seems you're trying to dump the blame on a lot of engineering problems you're creating for yourself on a tool.
People should just accept that they work with a legacy language and not try to turn C++ into Haskell, Rust or something it isn't. They just turn it into a worse mess than it already is.
Most C++ code that exists and which is useful is written in old school C++ anyway and could be continued to be maintained that way and wrapped for other users.
And really C++ performance is IMHO somewhat overrated. It depends entirely on what you are doing. I think it was the CouchDB creator. He made his first version in C++. He struggled hard. Then he switched to Erlang, despite Erlang running on a VM he got magnitudes higher performance and had to write less than half the code.
You know the ones who squeezed the most performance out of the Playstation 2 did it using LISP and not C++. They used LISP to create a DSL for PS2 assembly code. Thus they could do high level LISP coding as well as low level assembly all in one.
And today you got many scientists needing high performance computing switching to Julia. Fortran will usually outperform C++ on number crunching. And there are quite a number of cases where Julia will outperform Fortran.
Yes in real time systems with tight memory requirements something like Julia is not a good choice. But then again in those cause you may actually want to prefer C or Rust.
What I am talking about is that if you don't have to use a C++ library to solve your task, there is simply no sane reason to pick C++.
I interpret this assertion with a high dose of incredulity and skepticism.
You're specifically referring to a single class that's a part of C++'s standard library.
C++'s standard library is extremely spartan when compared with other programming languages, such as Java.
It's unbelievable that C++'s inclusion of stuff such as std::visit in its standard library renders it "too complex" when the extend and arcane nature of other standard libraries are incomparably higher.
Hell, some toolkits and frameworks are incomparably far more complex and we don't see them accused of being unusable.
I highly doubt people express in public that a particular library is too complex. They simply choose not to use it. I remember some Boost Graph library once. What a a mess. I never expressed publicly that I couldn't figure out how do use it. Nobody does that, because nobody wants to appear stupid. Instead I just wrote a graph library in C++ from scratch myself. Discovered that was faster than figuring out the template insanity in that Graph library.
I have worked on usability for a number of years and done usability testing on people. And rarely if ever do anyone ever blame the system. They blame themselves. Or they shut up because they don't want to appear stupid.
That is why when you do usability testing you have to sit an observe people. You cannot rely on questionnaires because people seldom admit the system was complex.
Actually observing a variety of people try to program modern C++, would likely have been a rather sad experience.
You may also want to look at this talk: "What C++ got Right" [3], where the guy shows some nice things that C++ does that other languages don't. The other languages versions of the code he shows does tend to be simpler and nicer, but the C++ versions tend to give you more control. Some people value that control (rightly or wrongly) and other languages that don't provide it will never feel right to them.
I do personally quite like Rust and I'm eagerly watching Zig and some of the other new contenders, so I'm all for moving to something more pleasant than C++, but modern C++ gives just enough new niceness that I don't feel pushed to jump ship until the alternatives check more boxes.
[1] https://github.com/skypjack/entt
There are some compelling ECS frameworks emerging in Rust if anyone is interested, though.
Legion -- https://github.com/amethyst/legion
Hecs -- https://github.com/Ralith/hecs
Shipyard -- https://github.com/leudz/shipyard
Specs -- https://github.com/amethyst/specs
Bevy -- https://github.com/bevyengine/bevy -- not strictly comparable as it's an entire game engine, but it's built around an (imo) more ergonomic fork of hecs.
Some benchmarks -- https://github.com/rust-gamedev/ecs_bench_suite
You could have used Godot, Unity3D and a whole host of other game engines if you wanted to make a game. If immutable data structures is your thing, wouldn't a functional language be a better fit, such as Haskell?
But I do know the problem you talk about. I have mostly used C++ in the past because you could not get any good GUI libraries in other languages.
I have played around with Zig a bit. Tried Rust earlier, but it seems to me to duplicate many of the things I didn't like about C++: slow compile times, complexity, verbose syntax.
Zig I am still making up my mind about. It looks to be on the right track. But for now I think Swift and Go are pretty good alternatives to C++, a bit depending on what you are making.
Personally I prefer Julia, but that is not exactly a drop in replacement for C++. You don't get fine grained control over memory usage and it is not suited for real time systems or small memory footprint. Zig is great at all those things though. Julia does replace C++ however in most case where you simply need raw number crunching performance, such as simulations, scientific computing, machine learning etc.
I however do use C++ to write servers and if I do not try to play language guru in my code modern C++ along with the STL feels incredibly easy to use.
The reality is that many projects are enabled more by existing libraries & tools rather than blank slate clean language syntax.
Some example domains of libraries & tools where C++ is the 1st-class client instead of Rust/Swift/D/Haskell:
+ deep learning: NVIDIA CUDA api is C++
+ HPC High Perfomance Computing: Intel MKL math library is C++
+ physics engine for video games: Unreal game engine is C++
+ latest cpu chip : ARM Allinea dev tools for the new ARM/Neoverse chips (e.g. latest AWS Graviton2 servers) is C++
+ Qt GUI is C++
Because of the extensive C++ ecosystem, you get a counter-intuitive situation where programming the latest cutting-edge apps requires a 35-year-old language from 1985 instead of the newer Rust language from 2012. I predict this delta of tools+libraries between C++ and Rust/Swift will continue to exist for 10+ years. Recommending/lecturing people to use Rust/Swift/D overlooks the reasons that many programmers use C++. It's not a random choice.
E.g., a realistic starting point for a project might be: "What's a good game engine... oh, it's Unreal, and that's C++." -- instead of -- "Rust has clean syntax of ownership and borrow with "&" ampersand symbol instead of ugly warts of unique_ptr<T> and std::move() in C++ so I'll code a game in Rust."
Sometimes, we look at the ecosystem first, and then work backwards from that to make programming choices.
It's when you have the luxury of a greenfield project that doesn't need legacy dependencies that choosing alternative cleaner syntax of Rust/Swift is possible. Amazon AWS and MS Azure are examples of doing new bare-metal projects with Rust instead of C++.
I find that statement very odd, because in my experience the bane of C++, and perhaps the primary reason to look for alternatives, is C++'s lack of a standard ABI.
The lack of a stable ABI is C++'s main source of bit rot, and the main technical point that justifies migrating projects, even legacy ones, into other programming languages. If not for the ABI issue, most of the technical points used in favour of jumping away from C++ into some bandwagon of the moment would be moot.
'paradoxically' would have been a better word choice on my part
And add critical third-party dependence?
And learn one more programming language that is rarely used?
Congratulations, your suggestion requires you to maintain two entirely separate languages, one of which is not standard nor mature, not to mention maintaining interoperability bugs.
"being rarely used is not a problem" it is a problem if I want to find docs, tutorials or people with more experience willing to help me
"instead of C++" as soon as something breaks I will need to debug both Nim and C++
The official tutorial and manual is great and comprehensive. There are many kind people with more experience on the Nim forum.
You won't need to debug C++, just like you don't need to debug JavaScript if you transpile to it from, say, CoffeeScript or Dart.
And yet you could argue that it's "adding further layer / critical third-party dependence" and so on.
It could be C++, if I would go with Unreal Engine.
I used Squirrel in past, solely because for some reason it was modding language of OpenTTD.
I am not fan of any of them, I am not an expert in any of them. But it was more effective that writing from scratch 3D engine or AI backend to use something else.
F# has been in this position: a smaller team that tries to align with C#, without a language implementation team backing them (unlike Roslyn-C#), yet constantly feeding innovations back into the dotnet ecosystem. You can see a lot of example where F# piloted the idea, and later C# adopted and extended it in its own way, and F# struggling to catch up in that OO-centric world
(Disclaimer: MS employee, but not on the language teams)
I need a good reason to use one more programming language and one more dependency.
Also, how many Unity examples and tutorials and knowledgeable people are using C# and how many F#?
As I said, sometimes for legacy reasons you must use C++. My point is to use a sane subset of C++ an ignore the modern crap. You are only going to make life harder for co-workers, and future maintainers. If the modern C++ features themselves are what attracts you specifically to C++ rather than some library you need to use, then that is when I suggest switching language.
> deep learning: NVIDIA CUDA api is C++
You can do deep learning much better in Julia. It is a nicer language, allow iterative development in a REPL and will typically give you higher performance as well.
And who on earth does machine learning in C++? People typically use Python today. C++ is an implementation detail. A necessary evil for a slow language like Python. But given a choice people pick Python as their interface, not C++, because C++ is a horrible language to use, which you should only use when you absolutely have to.
> HPC High Perfomance Computing: Intel MKL math library is C++
Again you could actually use Julia for that. Next generation climate models running on super computers is built in Julia, specifically because it is fast. A language with 1/10th the cognitive load of C++.
> physics engine for video games: Unreal game engine is C++
Sure, as I said, sometimes you need to use C++ for legacy reasons. But who on earth writes the game logic in C++? Typically you use Lua, JavaScript, GDScript or something else because people don't use C++ when they can avoid it. Historically it has been hard to avoid C++ when creating a physics engine, because you need the kind of high level performance and memory control C++ gives.
> Qt GUI is C++
Yes, which I have spent a lot of my career coding. And which cannot be compared to the insanity of modern C++. Qt tries to keep C++ somewhat sensible.
I could add VTK. Old school C++, which is fairly readable, unlike modern C++. As I said if you have some deep geek desire for fancy language features, find another language.
> Recommending/lecturing people to use Rust/Swift/D overlooks the reasons that many programmers use C++. It's not a random choice.
Problem is that you missed my central point. I didn't say you could go out and replace C++ in every instance with any of these languages. I specifically made it clear that for legacy reasons that may not be possible. And yes that may include that the functionality you need only exists in C++. My point was to write your code in cold old C++, which most people can actually somewhat fathom. If you want to geek out, C++ is a terrible place to do it.
> What's a good game engine... oh, it's Unreal, and that's C++.
Yeah... so Unreal Script was made because C++ is such an amazing language? You know I use Godot Engine, mostly written in C++, but I don't have to relate to that fact. I just code in GDScript. Fortunately the makers of Godot was no so sadistic as to require their users to code in C++.
> It's when you have the luxury of a greenfield project that doesn't need legacy dependencies that choosing alternative cleaner syntax of Rust/Swift is possible.
I think you are kind of overselling the need for using C++. You fail to mention in any of your examples that C++ is very frequently wrapped and not the preferred interface. E.g. TensorFlow is written in C++, but who the hell interface with it from C++? No, people use Python as much as possible.
Or how about Qt. The most popular way to create Qt GUI code today is through QML, not through their C++ APIs. You also failed to mention that in most game engines such as Unreal you can do almost your entire game in a script language. It is typically only the Engine designers who have to program in C++.
You have to consider each case, but frequently wrapping C++ works just fine and may make you a lot more productive. VTK is written in C++ but it is also frequently if not mostly used through bindings to other languages.
If you cannot wrap C++, then embedding a script language may work well. I have written simple game engines in C++ with Lua. I found myself pushing more and more functionality over to my Lua layer as I experienced how much productive language that was.
Long ago "modern" C++ crossed into a territory that I will never understand or use. STL is the fanciest stuff I write. Code needs to be maintainable. If I have to hire geniuses to collaborate on my code, that's not going to be cost-effective. So I use a conservative subset, and thank my lucky stars I don't need to pay attention to the latest exotica.
Do you know where the line is? C++11? Later versions?
I ask because I quite like a lot of the changes that C++11 brought to the language, I feel they make my code much simpler to understand and maintain, but I got very confused by post-11 C++ for a long time to the point where I said I'd never bother with C++17 or later (14 turned out not to be too scary once I actually sat down with it) because 17 and 20 added so much stuff that just looked beyond me. But I've since changed my mind and have started to experiment with C++20, after I sat down and read a few articles and watched a few cppcon videos on the changes. I don't know all that these versions have to offer, but I do understand the main changes enough to get by and most of them do make the language better, in my opinion.
Overall, the language is a mess, simply because they keep adding to it and very little stuff gets deprecated, removed, changed or cleaned. That leaves a large and messy language, especially compared to more recent deliberately-designed languages, but it turned out I could understand "most" of the things, once I had someone explain them to me. A few times ;)
Having said that, it probably isn't worth your time to "modernise" the C++ in your money-making codebase at this stage, if you're comfortable with it how it is. You likely wouldn't see a return in the effort it would take.
auto [a, b] = functionThatReturnsATuple();
For C++20, the one single thing I'm excited about is designated initializers (which was already in C99! It's kinda mad why this came so late). Basically you can initialize structs with parameter names with just a single line of code: struct Person {
std::string name;
int age;
};
Person person = {.name = "Bob", .age = 29};
Anything else from C++17 and onwards I really don't find a usage for right now. Compile-time programming using constexpr and consteval is great, until you realize that compile-time programming works under the hood by evaluating the AST directly (which is the slowest possible way for an interpreter), and would make your build times skyrocket to the moon. Concepts are kinda nice because of better error messages, but doesn't really matter for me if heavily templated libraries like STL and Eigen aren't fully ported for this feature. Ranges are just unnecessary for me, I learned the hard way that for loops are much better in terms of clarity and performance. And for modules: again, it only matters if the STL, tooling, and major libraries are ready for it, and I'm skeptical it will be achieved at any time.Funny thing is, I didn't even think of ranges while writing my comment above. I like the idea of them and they do allow some nice code, but its not something I've been wishing I had.
For C++17, there's a bunch of small things that I like: standard attributes like [[nodiscard]], nested namespace definitions are a nice convenience, constexpr if statements and some of the library additions are nice (string_view, optional, etc). Destructuring auto, as you said. Nothing as exciting as what C++11 or 20 adds, but still nice to haves.
Saying concepts are just for better error messages is like saying types are just for error checking. You can rely on them only for that. Or you can put them to work.
Most of what makes C++ powerful is from putting types to work. Concepts are just as powerful. You haven't seen yet what they can do.
Concepts are a fundamentally powerful tool. Haskell people will recognize them as akin to what they call type classes.
Thus, in P2187 http://wg21.link/p2187 we have conditional swap that, for any small- and simple-enough type, is swapped using CMOV instructions instead of a probably badly-predicted branch. This mechanism works in released Gcc-10 today. (Note that what goes into the Standard Library will probably differ, in details, substantially from P2187R5.)
Can you use this to compile-time detect if a class has a particular member function?
I've used code like this before, and the implementation has always been a bit of a mess, so I wonder if it can be simplified with concepts:
template <class T> void foo (T& t) {
if constexpr (hasMemberBar<T>()) {
t.bar();
}
}
Currently, I use this to implement hasMemberBar and I wonder if there's a better way now: https://gist.github.com/danielytics/8944b51ca51280fba30baab6... Especially something that works without using the preprocessor, while still not needing a new implementation for each member I wish to check for.The killer app is unique_ptr. Without move semantics, unique_ptr wouldn't be nearly as nice to use. There's a good reason that it has a move constructor but not a copy constructor.
> Do you know where the line is? C++11? Later versions?
It's funny to read this, because I purposely avoided C++ for the better part of twenty years after dabbling with it in college--not worth it to deal with so much complexity. I've started using it in the last couple of years for work, and the modern part is what makes it bearable.
C++ is a big beast, so there is a cost to each new standard simply by virtue of adding features. However, the types of things I want to use in C++14 are pretty simple and worthwhile; e.g., better constexpr functions, and the _t aliases (like std::enable_if_t<>, rather than typename std::enable_if<>::type). Similarly, C++17 has a few new templates, like std::optional and yes, std::variant, along with ergonomic improvements like the _v aliases (like std::is_floating_point_v<>, rather than std::is_floating_point<>::value).
Now that the glacial pace of standardization has been replaced with a three-year cycle, I almost want something like LTS releases. I use C++11, and a bit of C++14, but when is it safe to use C++17 or C++20?
And that is where I intended to stop, but then I started reading and watching content on C++17 and realised the things I thought were complex and scary were not so bad. I mean, there's much code I just don't understand, but from how it affects me personally, its not so bad and adds a bunch of improvements.
C++20 is a different beast altogether and I told myself its too complicated so I'll never bother beyond C++17. And then I watched some cppcon videos and realised that concepts, ranges, modules and coroutines are pretty cool. I still don't understand anything but the basics of concepts and ranges have some cool properties, but otherwise I dunno if I'll use them. But coroutines open a world of interesting possibility and modules have a lot of potential too, so I've started playing around with C++20 and will slowly learn to use it (although I did already notice some stuff that's not yet implemented in the clang version my package manager gave me... so not quite ready yet).
Move semantics and unique_ptr are really IMHO essential. Then lots of nice to have stuff such as, shared_ptr, threads, lambdas (make STL a lot more usable).
Personally I'd not want to be maintaining a C++98 style of code base at this point.
That being said you have probably already written all of the "nice to have" stuff so there's no real value benefit to go and replace it as such. So I understand your POV as well. That being said I'd probably modernize the code as I go whenever I touch any part of the codebase.
If you genuinely want your variant to just be a mildly polymorphic object, this is okay. But you probably don’t. Imagine the classic use of variants for a parse tree:
variant<string, shared_ptr<Node>>
Okay, presumably the string is a token and the Node is a subtree. This is already a bit unpleasant, but at least it’s serviceable. But then you discover that you really want tokens and symbol names to be separate. You try:
variant<string, string, shared_ptr<Node>>
And you lose, because the two unnamed string cases are ambiguous. To work around this, you need to wrap the strings in newtypes, which is quite verbose in C++. And you start to wonder why, after all these years, C++ hasn’t sucked it up and come up with real sum types in the language.
(For those who are too stuck in C++ land, this problem simply does not exist in a language like Rust.)
That is certainly an option, but actually std::variant<string, string, shared_ptr<Node>> does still work for most operations (std::visit is not one of them though). So another option is to use that typelist directly and switch to index-based access, at least for those entries:
if (std::string* s = std::get_if<0>(myVariant)) {
// Use s
}
else if (std::string* s = std::get_if<1>(myVariant)) {
// Use s
}
else if (NodePtr* node = std::get_if<2>(myVariant)) {
// Use node
}
else {
throw std::logic_error("Unknown variant entry");
}
At that point you'll probably want to define an enum (an old-school one so it implicitly converts to int) to identify the cases symbollically, which admittedly is pretty messy given that it's up to you, the maintainer, to keep the enum in sync with the actual type list in the variant. But it's hardly the worst coding sin ever, and better than any alternative I can think of (short of switching to another language). As you can see from my comment further down, I think a chain of if statements is cleaner than std::visit anyway, precisely because of the complexity problems of std::visit discussed in the article.Indeed, from the article: "In spite of all of this, I’ll be busy encouraging my coworkers to use variant if anybody needs me. Sum types are such a useful concept that they’re worth the pain[...]"
Just another opportunity for C++ geeks to show off.
Edit: I have seen Typescript that looks like this, but some of you could more easily corroborate this claim that are working with it more persistently.
Previously, I had been very diligent about using JSDoc to annotate methods and fields with types, which gave me a lot of information with which to work in my IDE. But JSDoc is not a type system, it's very cumbersome to express ad hoc object structures, and being "very diligent" is not "perfectly diligent", so the TS porting process discovered some subtle bugs where I had, for example, guessed wrong on the type of something coming from a library (I also forked and ported most of my dependencies), or changed the return type of a method but missed a couple of very rare calls of it.
Ad hoc union types in signatures gets super line-noisey. You can avoid it by diligently (there's that word again) using interfaces and named types. I think type expressions get overused in TS, out of some hope they can make the language into Haskell. That is not the case, and most of the time it's better to use interfaces.
Still, I have a dependency that can only be included via a script tag. This wasn't too much of a burden in JS, because the language does basically nothing for you. But using it in my TS port took a lot of effort. I had to download the source of it, patch in a tsconfig, generate .d.ts files, and fix them up where they had escaped out to `any`. It's still not perfect. The quality of the library is pretty low to begin with, but it's the only way to interface with a WebRTC server I'm using. I'm pretty close to deciding to use a different WebRTC server (probably even one I write on my own), just to fix the remaining issues.
That said, if you're starting from scratch, TS can be very clean and push you towards much better designs. It's hard to do the wrong thing. You can still do it, but it's hard. You need to have that ability, though, because there's just too much "wrong" with everyone else's code out there.
I don't mind that, too much. The only thing I mind is that it's very difficult to distribute a library in such a way that it can be used with a TS project without a lot of warnings or errors, especially if you're using Webpack (ugh, Webpack generates some uuuugly bundles). So even though a lot of my dependencies offer TS-generated bundles or .d.ts files, it still doesn't work very well. You really need to be distributing TS source for the library, and not relying on any conditional compilation that your consumers will have to replicate in their own builds.
Type checking has existed for quite some time. Those languages and developers have created some of the most static UIs you can think of. Those desktop apps got stuck, they don’t innovate or adapt. I think it has partly to do with how rigid and sedimented their tools and languages got.
I deal with it every day. We look at a Java enterprise app built, and go, yeah this has to be refreshed. That language has type checking. What are we replicating here? We throw that stuff away like it’s a joke, so I wonder if we aren’t wasting our time with some of this stuff.
Type checking defies the utter success of Python, Ruby, PHP and JavaScript in building the entire modern web. We moved quickly and accurately due to these languages (it’s not a coincidence that they are all interpreted). I don’t want us to forget that. If you guys want to add something that helps, sure, but if you guys want to add stuff that adds little value that slows stuff down, it will prove out the same way it happened before. We’re just going to throw it away when the seasons change.
More serious question for you: Why do interpreted languages exist? What are one of the things they shed? Why’d they get popular? Why’d people find a way to move fast in them?
I guess my vigilance is basically this, I’m the guy before you all turn JavaScript into what the OP is describing as the monstrosity that c++ is. Explain why that won’t happen the same way I guess, since we’re all smart, and students of history. We have something to protect here too. We built the stuff you guys couldn’t, but you know better apparently now. I’m your ghost of Christmas past.
std::variant<int, std::string> setting;
setting = "foo"s;
setting = 1;
Compared with something like: struct Setting {
Setting() { XXX }
~Setting() { XXX }
union {
string str;
int num;
};
enum Type { Str, Int, None };
Type tag;
};
Setting setting;
if (setting.tag != Setting::Str)
new (&setting.str) std::string;
setting.str = "foo";
setting.tag = Setting::Str;
if (setting.tag == Setting::Str)
setting.str.~std::string();
setting.str = 1;
setting.tag = Setting::Int;So you've got poor ergonomics, poor compilation performance, bloated code size, and poor runtime performance. I've written an enormous amount of C++ code from applications to libraries to system development. I literally have never found a place where `std::variant` has any positives that can't be written a different, clearer way with drastically better compile time and run time performance.
uh, I have yet to see variants beaten by inheritance in practice - in particular as soon as you have type erasure all your containers have an additional level of indirection (which can be solved partially by Boost.PolyCollection but that adds its own layer of complexity) and that has been a much bigger perf-killer - with variants memory is nicely packed when iterating over your array of values and that's by far the biggest performance thing you can do. You also need to allocate memory dynamically everywhere which is a no-go in a lot of cases.
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.
- you can write 10LOC to have a lambda visitor. Idk why the standard doesn’t do this for you but it’s not a big deal
- sum types are super useful and your language will want some sort of niceties/extra characters to deal with them (and all options will be better than unions which are an unsafe mess)
So ... what’s the big issue? C++ is complicated? It’s a language designed to give the programmer full control when they want it. You don’t have to constantly use every bit of it.
Compare template constraints in D and C++. C++ ends up with concepts after something like 25 years in the making, and they add huge amounts of syntax and bloat, whereas in D it's just an if statement after before the template body.
The way static if was considered was bizarre too
Anyway, until C++ gets pattern matching for the simple cases make_visitor is the best we can do.
If you want advanced variant use cases you can always use boost::variant (v1) which supports multivisitation, which coupled with templates is exceptionally powerful.
If you want to go even further with open multimethods you can checkout the yomm2 library.
As always C++ gives you options
Is there some kind of deeper magic going on in the "constexpr if" version than it looks like?
If you want full coverage then visitation guarantee that statically. Multi-visitation over m variants each with N possible types also does this statically with N^m instantiations. This balloons quickly, which is why I mentioned yomm2, which can add these handlers dynamically at runtime for polymorphic types.
(And thinking about this a bit more, there's presumably also the opposite case: if you add a branch for a type that's not a member of the variant / remove a type from a variant without updating all the visitors, it'd just happily compile with no indication that anything is wrong.)
And yes, C++ is an endless pit of horrors.
You may want your parse tree to produce objects instead of fundamental C++ types (e.g. with functions like isNull(), isValid(), etc.). A common approach might be to produce a simple AST where each node includes just the fundamental type you’re interested in. Then you can simply follow the visitor pattern and write visitors for things like serializing/deserializing, generating/traversing/executing trees/graphs/code, etc...
Perhaps I’m missing something though? A more concrete definition of the problem would be helpful...
But yeah the first time I had to use boost::variant (which is what the version in the standard library is based on) I thought the visitor was too much boilerplate. Things have gotten better since then, but many people are not even on C++17 yet ):
if (const auto sPtr (std::get_if<std::string>(&myVariant)); sPtr) {
/* use sPtr */
} else if (const auto iPtr (std::get_if<int>(&myVariant)); iPtr) {
/* use iPtr */
} if (std::string* s = std::get_if<std::string>(&myVariant)) {
// Use s
}
else if (int* i = std::get_if<std::string>(&myVariant)) {
// Use i
}
else {
throw std::logic_error("Unexpected type in variant");
}
The hardcore modern C++ gang will vigorously complain that using this kind of chained if will - horrors! - yield a runtime error instead of a compile time error if you have forgotten one of the types that the variant can hold. I have some sympathy for that point of view, but in practice the small likelihood of that bug happening and the small extra difficulty of finding it at runtime is completely overshadowed by the extra complexity of std::visit.I don't agree with GP's claim that chained ifs are likely to be slower than std::visit. If anything, it seems like they'd give the compiler less work to do to optimise well than the layers of funtion calls involved in std::visit, especially since std::visit is ultimately going to expand to chained if statements anyway.
(A digression that's maybe dangerous to bring up: You've made an odd choice out of the many initialisation syntaxes to use. Many in the hardcore modern C++ gang would advise always using auto and brace initialisation, with no equals, even for really simple variable definitions e.g. auto i{int(3)};. Personally I find that obtuse and would write that as int i = 3, at least for simple things (raw pointers like in the example above fall into that category for me). Direct initialisation with parentheses isn't normally advocated by either old-timers or modern C++ fans, in part because of the most vexing parse - did you mean to use braces?)
Re: compile time vs runtime errors, it is a valid point. In Scala (which I use in my day job) I will generally question any use of `case _ =>` in a pattern match because of this, particularly because you don't get the nice compiler error when someone (for example) adds a new type to a sealed trait hierarchy without making sure all business logic that touches that type handles the new case correctly. I guess in C++ it feels painful for a non-expert like myself to get those types of compile time guarantees while functional languages like Scala (for example) make it easy.
I mean, I can grok it, but I’d never write it. I’d fall back to the ‘write methods to encapsulate the behaviour’ approach instead, and I think my co-workers would thank me for it.
so... it's a no-go ? if you don't care about wasting memory just use python or JS
#define MATCH_VARIANT(x) [](auto& x)
#define CASE_VARIANT(x, T) constexpr (std::is_same_v<std::decay_t<decltype(x)>, T>)
MATCH_VARIANT(arg) {
if CASE_VARIANT(arg, string) {
printf("string: %s\n", arg.c_str());
// ...
}
else if CASE_VARIANT(arg, int) {
printf("integer: %d\n", arg);
// ...
}
else if CASE_VARIANT(arg, bool) {
printf("bool: %d\n", arg);
// ...
}
}
(MATCH_VARIANT could be extended to allow capturing vairables, or just omit that macro, it saves less typing than CASE_VARIANT anyway.)https://en.wikipedia.org/wiki/Substitution_failure_is_not_an...
I feel like except maybe in complex video-game engines, C++ is slowly but surely getting replaced by more adapted languages.
Here on HN you can always get upvotes by promoting Rust (and it really is the only interesting new systems language in 30 years) or complaining about C++, but C++ is going nowhere but upwards. C++20 is better than '17, and C++23 will be better than '20.
A language has achieved something when there are more articles complaining about it than about having succeeded in coding something using it.
However, if someone were to ask my advice on which programming language s/he ought to learn, I usually point to python as its easy to begin and unless the goal is to get into game development, financial institutions or something like that, where C++ is the dominant language, I do not recommend it. It takes a lot of time before the complexity begins to disappear from one's mental model of the language. After that point though, one can see why it's a solid language.
https://github.com/insieme/insieme/blob/master/code/utils/in...
Some tests illustrating the usage can be found here:
https://github.com/insieme/insieme/blob/master/code/utils/te...
If you compare the sum type and pattern matching story with modern languages, sure, you will find C++ is lacking. And it is. However, such a comparison is ignoring history.
I actually like variant. It is the best sum type we have in C++.