68 comments

[ 3.3 ms ] story [ 125 ms ] thread
Why is C++ so complicated?

My initial hypothesis was that the language was evolving to simplify how a someone can stitch together multiple libraries to create an application. If the complexity can be reduced (by say, adding garbage collection), then the quality of programmers a software company would need to hire can drop from the Donald Knuths of the world to someone who is more likely to submit their resume at an average software firm.

However, at this point, it is safe to claim that the complexity of keeping up with C++ far exceeds the complexity of the original problems it was trying to solve.

Does anyone have a good argument for making C++ so convoluted?

Backwards compatibility with C89 copy-paste semantics, which is only simple for those that never read ISO C documents, and the goal to design a powerfull expressive language, that allows any developer to create libraries, which in other programming languages require compiler and runtime support instead.

Plus even other languages that appear simple, are only so for those that don't really know the complete language standard.

Python is my favourite example, as it is demeed as simple language for begginers, yet the language + library reference PDFs are also a couple of thousand pages long, and there are so many PEPs that hardly anyone can pinpoint which is the minimum Python version to execute a random piece of sample code.

C++ is a large toolbox. You don't have to use all of it, and probably shouldn't use all of it in one program.

There are other approaches: e.g. js + npm, which can get you super rapidly to something functioning. It has different risk points than C++, and different benefits. If it works for your use case you'd likely be a fool to use C++, especially since its "standard" library is so much larger. But likewise there are complicated problems for which you'd be wrong to use node.

Transportation runs from bicycles to mack trucks to tanks to spacecraft, each with different complexity and performance issues.

What it means is that you can quickly write a powerful program or a team can build a large, maintainable system. Some people on that team might use some of the more abstruse tools to make stuff (specific macros (aka templates), other complex objects) that create common resources used across the team.

You can choose a simpler language but then for some cases every user needs to repeat certain boilerplate and/or risks forgetting some important corner cases.

You can let everyone free to use any arbitrary tool in the toolbox but then you equally end up with an unmaintainable mess.

This really isn't any different from building, say, a power plant. Some people have gone to a lot of work to develop the steam schedules (regulatory code, such as schedule 60 piping) and you can buy them and have people who know what they are doing weld them up as you need them, but you don't have to design the safety code, simply be sure you're within its constraints.

It was Jonathan Shopiro (author or iostreams) who interrupted a comparison equating Language X with a seaplane, interjecting that by such a standard, C++ would be a boat anchor with feathers.

It remains the funniest thing I have ever heard said about C++. It completely derailed what had been becoming a tedious conversation. I don't recall what Language X was.

I have to disagree. From where I stand, complexity of C++ merely reflects that of practice of programming in general (modulo the language's historical origin and evolution). C++ remains a pragmatic language that helps solving practical problems. Memory management is one aspect, and the RAII paradigm and smart pointers are an excellent way to go about it; garbage collection libraries are also available.
I don't think C++ is actually that complicated. However, I do think C++ tries to use way too many different permutations of the ()[]{}<> characters. Once you try to stuff too many of them into the same piece of code, you end up having to read code twice, once to figure out what the braces are for, again to figure out what the code is doing.

As an example, the < and > characters may indicate comparison (< and > in boolean expressions), a template declaration (< and > with a comma separated list following a function/class name declaration), a template specialization, an operator overload, an object access via pointer (->), a bit shift, (<< or >>), and possibly writing to a stream (<<).

A less egregious example, [] might be used to indicate indexing into an array, indexing into an object via overloaded [], declaring an array type, or declaring the captures in a lambda. While you can figure it out contextually, it's often irritating to have to figure out context first and then interpret the code.

They consciously try not to add many keywords or reserved characters. That has the downside but so does the alternative.

I do agree that the stream operators << and >> was a bad experiment in retrospect.

Backward compatibility is a hard problem.

Look at rust, or python, those are great languages, but obviously you need a new compiler and you need to write new code.

With C++, you can just add new code to an existing code base, and share that code across projects.

What is nice is that the language is evolving, but previous code still works. So the problem with C++, is adding new cool features, on an existing language, while not breaking existing code bases.

That's how you improve a language, by keeping its users while adding new better ways of doing things.

I honestly don't think rust or go will really survive over the decades. C++ already has momentum, and honestly, its close-to-C syntax is still a good thing because it's intuitive. How down-to-earth simple do you think go or rust are for students? How do you wrap your head around having to explain students the purpose of a garbage collector, not to mention how it will behave and how predictable it will be?

> If the complexity can be reduced (by say, adding garbage collection)

Adding a garbage collector would make code more complex, not simpler, as it would break RAII. I spend a lot more time dealing with memory and resource allocation issues in GC languages than I do in C++.

In languages like Python we have to use `with`, and this is harder to use in the client code, and the class is much harder to write, than anything in C++ where we just add a destructor to clean up.

    std::vector<char> data(10 << 20);
    std::ifstream("/dev/urandom", std::ios::binary).read(data.data(), data.size());
The above code can never leak (either memory or file handles) under any circumstances and the implementations of the types involved push zero overhead/boilerplate on me as a user of them.

I think if you want to reduce complexity of C the place that everybody else is looking is at how to allow opting in and out of legacy support.

As someone who is on a personal journey to transition from c with classes thinking to modern c++ I can see why there's so much hate from non-cpp developers about how these features look ugly.

But, as a developer, who has dealt with an absurd amount of bugs that can be prevented from these modern c++ features I really like this.

I went the other way, started writing C++ in 95 and digging into C properly around 2005.

Just make sure that the cure isn't worse than the disease. It's all to easy to slip into identifying with C++ because of the substantial effort it takes to master.

Should you, like me and many others; eventually find yourself spending more time and effort on taming C++, don't hesitate to let go. It's a tool.

The solution is easy, managed languages for 90% of the code and C++ for the remaining no-excuses full throtle execution speed, while offering reasonable safety for arrays, strings, out parameters, enumerations, lifetimes.

Not everyone is running Solaris on an ADI enabled SPARC.

I have a somewhat different perspective to offer: C or C++ for components (effectively 90% of all code), a managed or scripted code (10%) for the glue.

The reason is that the modern C++ is perfectly suitable for high-level programming -- on par with Java, C# or Visual Basic. No "taming" is needed.

Taming is necessary, because regardless of what you and me think about modern C++, there is a large crowd that still writes C with C++ compiler with total disregard for any kind of security validation tools.

So the only way to prevent such security exploits is by preventing copy-paste compatibility with C, which C++ sadly cannot do without breaking backwards compatibility.

You can see this happening across all desktop and mobile oriented OSes, where both C and C++ have been reduced to high performace low level OS layers, with userspace migrating to something else.

Microsoft is the only vendor left on OSes for the consumer space that still cares enough to support C++ on their UI toolkits.

Apple supports Objective-C, which is arguably just as bad as C++.
It does, due to its NeXTSTEP legacy, but the roadmap is quite clear.

"Swift is intended as a replacement for C-based languages (C, C++, and Objective-C)."

Taken from https://swift.org/about/

"Swift is a successor to both the C and Objective-C languages."

Taken from https://developer.apple.com/swift/

I would be very surprised if Apple’s frameworks ended up becoming Swift-only any time in the next decade. While Swift is the spiritual sucessor to Objective-C, there is far too much Objective-C already written that would be costly to obsolete, even if Swift had the facilities to do everything Objective-C can (and, as I’m sure you’re aware, it is not at that point yet).
Sure, but that will eventually happen, even if they do just a couple of components per OS release.

Launchd, dock were announced as rewritten at WWDC 2017, the new XCode build system and instrumentation at WWDC 2018, and it will continue little by little.

Naturally it won't happen overnight, but Apple isn't known to endure Python 2/3 scenarios for that long.

Those are not frameworks, those are apps that ship as part of the system. Apple has not and seemingly will not deprecate Objective-C for third party developers. They haven’t even rewritten any significant code in Swift, IIRC. Maybe parts of launchd?
Timac analysed several MacOS and iOS versions for swift usage, and its growing through the system surprisingly rapidly for a language awaiting a stable ABI. Here’s the most recent analysis, which also links to others: https://blog.timac.org/2018/0924-state-of-swift-ios12/
Yup, I’ve read those and that’s what I based my statement on. Apple doesn’t seem to have rewritten any code in Swift “just for the sake of it”; they have slowly introduced it the in places they could to add new features.
They need to start somewhere.

At WWDC 2017 they mentioned that launchd was fully ported. The video is available.

> C++ is perfectly suitable for high-level programming -- on par with Java, C# or Visual Basic. No "taming" is needed.

This is completely untrue.

This is the "right" approach - speaking from experience after having tried both ways (10-90 vs 90-10). The general structure is that your application is built as a DAG with the nodes representing C++ functions (not methods on classes), with edges representing arbitrary datastructures (not objects), with the graph being constructed and hooked up in python and executed using a library like TBB
This just doesn't work for making fast software in my experience. It's very rare that 90% of your execution time is in 10% of your code because that code is obvious low hanging fruit and will have already been optimised.

Selective optimisation of hot code will always lead to execution time being spread evenly through your code.

My experience replacing software 100% written in C++ by a mix of JVM/.NET + C++ shows otherwise.

Most of the time, the C++ help isn't even necessary anymore, unless the systems were doing some GPGPU stuff, real time audio or high performance graphics.

That's just because Java and C# (or whatever) are fast languages. C++ is only a little faster. Try it with Python or Ruby.
From my point of view Python and Ruby are only worthy of shell scripts, and scenarios where performance isn't even part of the requirements list.

Using them in scenarios where performance matters is the typical everything looks like a nail.

Feel free to disagree, my experience during the early 2000 with Tcl has taught me to never use languages without native support for JIT/AOT toolchains ever again when performance matters.

Anything related to distributed computing, performance does matter.

I prefer embedding scripting languages [0] in C. Which is another path to the same kind of compromise, with the added advantage of controlling the world for the scripting language.

[0] https://gitlab.com/sifoo/snigl

I work in both C and C++ code bases so I get to dive deep into both sets. Only find solutions and language idioms when an actual problem arises.
I'm just thinking what are all these letters on my screen. The first code example in the blogpost look like Javascript callback chains.

These new right left-values, special rightvalues etc make my head hurt. Template programming makes my head hurt. I can't look into fancy cpp code and understand it anymore.

I'm wondering have new programmers are supposed to catch up too all the things in cpp even now and the mountain is getting ever bigger.

in early 2017 we started a project in c++17 (the compilers we cared about, g++ and clang++, by that time supported basically all of C++17 and everything in the library we needed except std::variant). It was a "start with an empty emacs buffer" project so we could start straight there.

The trick was to treat it as a brand new language that happened to have some resemblances to other languages some of us already knew (i.e. C, C++, etc). C++17 is actually a pretty expressive language and pretty fun to program in.

I recognize most people don't have this luxury. Interfacing to the small number of external packages we used involved making modern "cutout" connections to the legacy packages, which actually was worthwhile.

Hi Gumby --

K. was just asking how your new project has gone.

Reading the post, I thought, "hey, that sounds like Gumby", and then looked at the byline.

I am running C++ on an Arduino now, and prepping to teach my son to run it on a Blyst Nano. He wants to make his own smart watch. He will end up doing a lot of very educational yak-shaving on the way.

Hope to see you again sometime soon.

N

I’ll send you a note.
I feel like a development such as the C++ standard needs to deprecate things a lot faster than they currently do. Imagine in 15 years time when C++ compilers are so complicated that they become self-aware and decide to get rid of us programmers.
> the C++ standard needs to deprecate things a lot faster than the currently do.

This, a thousand times. It's all very well for gumby to say that C++ is a big toolbox, but a good toolbox doesn't have all the mismatched tools from ten previous toolboxes all thrown together. Longcommonname can say that using modern C++ features help prevent errors, but that statement becomes a bit problematic when the most modern C++ style becomes deprecated C++ every few years. As the OP aptly illustrates, there are just too many ways to do even fairly simple things. All were idiomatic "best practices" for their time. It's not a problem if there are many ways to do things if they're all comprehensible to someone who knows the universal core of the language (as is often the case e.g. in C or Python). It's a problem when understanding each of them requires a whole separate excursion through some separate specialized and time-bound part of the standard, yet they all exist together in any large codebase that has had to build on those shifting sands. That's not good for maintainability. Future programmers will misunderstand the intent of code using each transient style, and either break it when they change it in place or miss some important detail when they try to replace it.

Different versions of C++ should remain separate to a much larger degree than is currently the case. Trying to combine several distinct language-as-written into a single super language-as-compiled (a mistake also made by Scala) never seems to work out very well.

Do you interact with any significant C++ code base? Because I'm having a hard time seeing what your practical recommendations would be past the idealism.

Yes, you can have major breaking changes every few years. But do you really think every larger C++ code base being stuck with the C++ version it was founded on is the way to go? Should they all be rewritten for each version?`

Don't get me wrong, I'd love if we could deprecate more old stuff. The problem is that it doesn't just disappear from existing code (of which there is a _lot_) by marking it as such. If there are straightforward replacements (e.g. auto_ptr -> unique_ptr) that's one thing, but unless you want each new C++ standard to be unusable with existing code, you can't just do that.

> Do you interact with any significant C++ code base?

Yes. It's the codebase for one of the largest data storage systems in the world. If it stopped working, literally billions of people could be affected. Is that good enough to get past the ad hominem?

> past the idealism.

Is the idealist the one who questions the wisdom of changing a codebase that already works, or the one who demands it? It's all too easy to accuse others of being too idealistic, or to make up strawmen (see next point), but I don't think those are very constructive ways to approach a discussion.

> do you really think every larger C++ code base being stuck with the C++ version it was founded on is the way to go?

Of course not. But the transition from old idioms to new ones can be managed. The first thing that has to happen is that the people adding new features to each standard must be explicit about which old features are being deprecated when. Sure, you can specify --std=c++25 to get the new hotness, but then that might preclude use of old feature xyz from C++11 in the same compilation unit. You have to choose, and you should choose, according to pragmatic needs instead of mere neophilia.

> unless you want each new C++ standard to be unusable with existing code

Controlled deprecation doesn't necessarily mean 100% incompatibility between versions. N-1 is a very common model, which can be extended across any K prior releases. There are tons of examples, not only from languages but from APIs, file formats, operating systems, and even other kinds of engineering besides computers. People should absolutely be given time to adjust, but the time should be finite. "Simultaneously support everything that every existed" is the one model that's least likely to work in the long term.

Fortunately the ABIs are pretty stable so you can compile an old module with C++03 and link it into your c++17 program.
> It's all very well for gumby to say that C++ is a big toolbox, but a good toolbox doesn't have all the mismatched tools from ten previous toolboxes all thrown together.

Regrettably they typically do, as well as three kinds of the same kind of screwdriver, with slightly different lengths (c++'s equivalent would be 'for', 'while' and 'do..while'.). One of the appeals of a new language (e.g. rust) is that you don't start with the legacy boat anchors...yet (look at Python or perl).

I don't think there's a very good solution.

Go tried to go the other way by explicitly not having lots of affordances. Personally I'm not a fan but I can see the logic.

> three kinds of the same kind of screwdriver, with slightly different lengths

Yeah, true. Maybe the better analogy is not the toolbox but the screws, bolts, etc. that the tools operate on. I have several sizes of Torx and other exotic screwdrivers which I have never needed ... but I might some day, if some idiot who knows where or when decided that they just had to use that particular screw for something. In large long-lived C++ codebases there always seems to be some asshole who used the equivalent of a MorTorq or Tri-Wing just because it looked cool, so that feature has to be supported forever after. Programmers years later then see this far from self-explanatory code, because these additions always seem to involve using familiar symbols in arbitrary new ways, and have to pore through old versions of the standard to figure out WTF it does. I do think it would be far better if the old screws and the drivers for them were deprecated from time to time.

Yeah, the distinction between statements and expressions is an inherited botch that should never have existed in the first place.
So much of C++'s value proposition is based off the fact that it has such strong legacy compatibility. It's a language that is used today by businesses who are risk averse. Deprecating parts of the standard will make C++ a much less attractive language for the people who make up the majority of it's current user base. You also can't deprecate anything required for C interop, like the preprocessor.
Is modern C++ the new Ada?
Almost, copy-paste compatibility with C89 hinders its safety.

However it is better than keeping with plain old C.

I think the biggest difference between Ada and C++ is that much of the functionality in C++ is implemented in the form of libraries vs. something that is built into the language itself. Character strings is the most obvious example, but there are many others (such as concurrency primitives). Personally, I am not sure which way is better: on the one hand, a good language should, indeed, give the programmer powerful means of abstraction; on the other hand, it might be beneficial if the language grammar could somehow directly absorb, in time, things of unquestionable value - so that the construct's syntax could be simpler or more natural, or, perhaps, so that the compiler could employ better optimization techniques.
Nah. Ada is the new Ada. C++ is a different beast altogether.
> Is modern C++ the new Ada?

I don't know much about Ada except what experience I've had with PL/SQL, which is supposedly related to it.

I found myself counting the number of characters I'd have to change to get that first C++ snippet to evaluate as ES6.

No. Ada might be too complicated, but it's far more coherent. C++ is like a novel with each sentence written by a different author. Even worse, each author keeps trying to pull the plot and the characters in a very different direction. Any C++ codebase that spans more than one version of the C++ standard ends up combining the two into its own special dialect, defeating the whole purpose of having standards in the first place.
In both the original example and this updated version, the nested loops (or foreach calls) create generators on each iteration. However, the first example is creating the generator chain more implicitly than the newer example. Since the original example also uses a coalescing expression (i.e. I mean that he is returning a dynamically-created generator chain as a variable) it means the use of "return" keywords at every subcall to foreach which can confuse programmers that aren't as seasoned with generators or functional generator patterns. It's actually, I would argue, not "bad code" but rather a different approach that procedural programmers probably aren't used to. The benefit of this style of doing things is that it comes with the benefits that usually come with creating lambda expressions. There are better examples of creating on-the-fly generators, and surely thats what the original author wanted to showcase, but to programmers not well-accustomed to common range-based generator patterns, the Pythagorean triple example for this looks and feels a mess when it's clear you don't need to generate the generator dynamically. This updated example I think is more generally useful for showcasing the range iterators to programmers.

Also this is just my opinion. Of course it could be that the original example is hated by all and a total POS, but to me it seemed quite clear in a functional context what it was getting at and what it meant for use of dynamically-created generators in higher order functions.

Agree. The "bad code" example looks beautiful to me, albeit unusual for someone who is accustomed mostly to the imperative style; I have no doubt that it would only take a small amount of (self-)training to absorb such style to the extent that it becomes easy to read.
you could have put "beautiful" in quotes as well. These type of discussions are too subjective to have meaning to engineers. Every project has its guidelines and standards. This is the same problem with python, the language is trying to dictate too much.

If someone makes a bunch of oil paint pigments they are not supposed to dictate how the artist uses them.

Let the engineers and individual projects figure out how best to use the tools of the language. Any attempt to create coding standards across the board is an attempt to control others creations.

If its really good style you would not use the word "beautiful" you would use the word correct , no quotes. Engineers dont deal in vague ideas.

As a full-time C++ developer, my only gripe with features like these is that they require nontrivial compiler optimizations in order to produce the equivalent "old-style" high-performance code. That leads to two issues for me:

1. Longer compilation times

2. Your program may be too slow in debug mode for you to meaningfully debug it

We're kind of at a crossing point where I would love it if I could have a compiler that "de-sugars" these expressions into optimized C code, i.e. removes the lambdas, adds local variables, etc. and then compiles with no or minimal optimizations for debug mode. I really love gcc's -Og mode, which should do something very close to that, but I still have to read the assembly code to get a full picture of what's happening. I feel like we could do a little bit better.

Edit: And if you're wondering why I care that much about performance, it's because there is really no reason to use C++ today other than to write high-performance software. There are other languages that can get bare-metal performance and I don't want to insult anyone using Fortran, Ada, etc. plus I'm keeping a close eye on Rust, but the combined tooling that exists for C++ still makes it a better, more productive alternative as far as I can judge. The other reason to use C++ would be to interface with a C++ system, in which case it might be easier to just write everything in C++ and skip the translation layer pain, but you might not care that much about performance in that case.

As a C++ compiler developer I don't think these features are what's causing long compilation times. Most of them don't even require any changes to llvm. AFAIK ranges doesn't, and the changes necessary for coroutines are simpler than you may think.

Next time you have a program that takes a while to compile, try profiling clang with perf! It's super interesting.

Honestly, I don't understand how the coroutine version is easier to read, or what was confusing about the original example.

Either you grok yield or you don't.

I find it's a lot more boilerplate, more visual overhead in finding the important bits in the sea of various brackets... But maybe I'm just blinded by imperative upbringings.
Great example. The first one was very hard to understand, and this one made it a lot more clear.
That’s an interesting perspective; I found both to be similarly complex. Both boiled down to “lazy generators”, albeit with different syntax.
Looked at the code and my current endeavors to learn Rust are reinforced. I'd prefer to never touch that abomination of a language that is C++ ever again.
my biggest problem with modern c++ is indeed readability.

c++11 remains to be the most popular language in algorithm competitions(e.g. usaco), 51% usage there, along with 33% java and 12% python, which surprised me

Off topic: the code examples don’t scroll on mobile
Where are the local variables stored for a coroutine?

I do like new advanced features but we must not let them make us lose sight of the metal.

Really weird to read this and not a single mention (in the article or ITT) of the most critical concept here: Composability. That is what ranges can do for your code -- make it composable.

The imperative-style examples don't compose in any meaningful sense. The only thing you can really do with the imperative style programs is to sequence them, i.e. run one after the other.

With ranges you can apply various types of transformations, combine multiple ranges, it lets the consumer(!) decide how much work should be done, etc. etc.

The gives incredible leverage for code reuse.

> The only thing you can really do with the imperative style programs is to sequence them

What's wrong with that? Isn't that the most readily accessible (and therefore maintainable) kind of composition? I realize that some people might prefer other forms of transformation or combination, but can you give an example of where it provides a tangible (not just aesthetic) benefit?

I answered that in the following paragraphs: It gives you a huge amount of potential for reuse -- essentially for free.

(Alright, this is C++ so there's a little bit of cost due to a tiny amount of extra 'syntax' where C++ imperative syntax is "free", but that's trivial for any but the most trivial of examples.)

"Following paragraphs" is an ambitious term for barely one line of text. You have claimed there is some unspecified promised land of reuse through unspecified transformations and combinations, but you really haven't shown anything that's not equally achievable with "mere" sequential application. It still looks like a matter of aesthetics - that you prefer a more syntax-laden functional style, but not one that's objectively better.
Let's define "objectively better" as "allowing to do A, then B as well as to do B, then A - rather than only allowing to do A, then B". Then composability - defined as "allowing to do A, then B as well as to do B, then A" - is objectively better than imperative style - defined as "only allowing to do A, then B".

In these terms it looks like a tautology. But the essense is still there - if for some reason you'd like to have a freedom of order of some transformations, you need composability, as an opposite to an approach which prescribes the order.