C++ and the Culture of Complexity
blog.greaterthanzero.com
blog.greaterthanzero.com
That the author thinks C++ is competing with Go betrays a narrow perspective. There are many important things you can do in C++ that are not possible in Go, in part because of the features that make C++ "complex" that simpler languages do not have. As an obvious example, the renaissance of C++ for high-performance server systems is based in part on the fact that garbage collectors are a major drag on performance for these kinds of codes.
Which is not to say that C++ is not excessively complex, just that the author seems to lack an understanding of why some of that complexity is essential functionality.
However, C++ occupies a sweet spot whereby it allows a lot of control, but also a lot of abstraction and structure--much more so than C, in my opinion.
Or, put another way: one doesn't build a skyscraper out of playdough or cinderblock (easy to use materials), but one doesn't mill the entire skyscraper out of a solid block of steel, either (that would be unjustifiably difficult). C++ is the language of skyscrapers.
C is nicer in that its ABI is so simple that it's practically the lingua franca of inter-language linkage, but the fact that pointer types convert automatically to other pointer types is a huge source of errors, and so is the lack of language support for things like destructors.
Additionally there are many C++ concepts that cannot be implemented in C without the use of macros everywhere, such as using a sorting algorithm (which in C requires either a pointer indirection for a function call, or use of macros to generate a new sort function for each appropriate type).
Additionally in C++ you can use things like type expressions to select and compile the optimal version of a matrix-vector multiplication automatically, and I'm not even sure you could do that in C with macros as it involves programmatically generating types at compile-time.
C is certainly nice for some problems but your comment applies as much to going from C to asm as it does going from C++ to C.
Over the long term I think Go is going to offer a much more sane environment for implementing high performance servers.
It also allows for running just the constructor in an area of already-allocated memory, if you're particularly crazy.
With that said it's hard to argue that Go makes concurrency easier at this point, which can certainly make it more useful for many of the types of problems programmers are seeing today.
It is difficult to implement multiple allocation strategies in the same application. In another time, I actually did quite a bit of work in this area: http://accu.org/index.php/articles/1308
Do you mean something like operator overloading where a given type has a different implementation of new that is used for it? Or a way to select different implementations of the global operator new at runtime? It seems to me that it should be possible for the latter to make it switchable, although the code would be inelegant to say the least.
AFAIK the former is possible as well, but the problem is that I think the type-specific operator new/delete have to be members of the type which means that you can't add it on later.
That's a great article btw.
That would be a gigantic change to the language, which I'm sure I don't understand the implications of.
After thinking about this some more, I'm going to stand by my original, even if poorly argued, comment that C++ doesn't really live up to the promise of "precise control" when it comes to memory allocation, and this is one area that can have a big effect on performance. C++ has all the overhead of a low level language, with a promise of control that when you really need it is often difficult to access.
The problem with C++ is exactly the opposite, it has too many options, which makes it difficult to create a global memory management policy, unless you define you own complete framework. Quite the opposite of what you said.
I won't opine on whether it actually is unnecessary.
Now you're correct that the standard library provided with C++ does not quite give you this same level of control. If you want node elements placed somewhere specific you either have to pick the right container, write a custom allocator (including rebind support so that any required container-internal elements also can use your allocator), etc.
But this is a complaint about a C++-only feature that C doesn't even provide.
On the very few times that C does provide an standard library function that will automatically allocate storage from the heap, it is even less customizable than the C++ equivalent. Normally your only option is something allocated with malloc(3), you're told to use free(3) when appropriate, and if you need something different you run into trying to replace malloc/free and friends and then somehow telling your custom malloc when to use custom behavior and when to use stdlib behavior. But you could do this in C++ code too.
Now you can certainly easily write your own library with better support for custom allocation/deallocation. Things like the Apache Portable Runtime, for instance, or glib.
But once you've bought into the idea of simply ignoring the language standard library anyways, you've bought into something else that you could do in C++ if you really felt like it were useful.
The problem with C++ is that the standard library makes it almost straightforward to do what you want and still be able to use standard containers without having to write your own, but we have to remember that we're talking about a problem that essentially can't even be spoken about in C. So it's not so much that C "doesn't have a problem", as that this problem couldn't even be expressed in C.
Are you trying to argue that C++ programs can't use custom allocators because of language restrictions? That's demonstrably false; Gecko and WebKit, just to name a couple of C++ codebases I'm relatively familiar with, use custom allocators everywhere.
The tricky part, to me at least, is when overloading. It's possible to overload new in a way that "new int" calls the original new, and "new int, foo" calls your overloaded allocator (foo could be an object used for the sole purpose of knowing which new to call, or it could actually be used by that new -- maybe it's a memory pool). That's easy enough to do. The surprise comes from the fact that while you can overload operator delete, it's a real pain to call the overloaded version (plus, of course, you have to remember which memory came from which version of new). Assuming you use "foo" to specify your overloaded new and delete, "delete p, foo" is a syntax error. You need to call something like "::operator delete(p, foo)". At least that was the case with C++03; I don't know if C++11 changed anything. Personally, I just overload new, but call a destroy() function instead of an overloaded delete.
At any rate, Go certainly does offer some nice tradeoffs vs C++.
I don't quite follow, who's stopping you from using sbrk(), mmap() and all the other low level allocation system calls if you really have to? AFAIK, many high performance applications have their own allocation mechanism. Furthermore, new/malloc is worlds apart from GC when it comes to precise memory management.
Now, to be fair, I have no experience implementing high performance servers. But speaking for games, GC gets in the way a lot. You can make it work by essentially avoiding allocation altogether, at least on systems where you can use tons of memory, but that's just a very crude workaround.
From a pure language perspective, nothing.
From a standard library perspective... it's probably more difficult. I won't say impossible, as the STL allocator concept certainly seems general enough to allow this, but definitely more difficult.
In 2013, not a lot of problems require that level of control.
First, you can create a lot of objects on the stack, which avoids any dynamic allocation at all and is far, far faster.
Second, you can precisely control the layout in memory of your data structures. For example, you can represent a set of vertices in an OpenGL scene as a contiguous array of structs. This gives you much, much better cache performance and also makes it easy to hand data directly off to hardware components like graphics cards that need input in memory formatted in specific ways. In languages like Java you have to resort to awkward, inefficient hacks like FloatBuffers to do the same thing.
[1] http://www.intel.com/content/dam/www/public/us/en/documents/...
The complexities of C++ almost always arise out of the extremely high degree of power and flexibility it offers programmers.
The complexities of Java end up having to do with hyper-"architected" class libraries, rife with excessively-used design patterns to the point of being incomprehensible.
JavaScript's complexity arises due to core functionality that's missing (such as proper class-based OO, namespaces, and proper support for modularity), or core functionality that's limited in practice (like it's prototype-based OO), or core functionality that's unjustifiably broken (its comparison operators, semicolon insertion, its scoping, its type system, its awful standard library, among others).
Out of those three, JavaScript's complexities are by far the worse. The flaws are outright stupid to being with, and there's nothing that can really be done to avoid them in many cases. At least Java programmers can choose not to create and use bloated class hierarchies, for instance. And at least C++'s complexity offers superbly powerful features and excellent performance, and at least it's understandable how and why this complexity thus arises.
The Harmony work is somewhat of a step in the right direction, but its impact will still be quite limited, assuming it ever does become a standard.
C++, on the other hand, has seen significant improvement over the past two decades. Much of its complexity can now be avoided when using a "Modern C++" approach, all without losing access to its powerful functionality in those cases when it truly is needed.
C++ has evolved in a way that makes it more usable. JavaScript has merely stagnated, without the community showing any real interest in cleaning up what's a very unjustifiably bad situation.
There are certain aspects of C++'s complexity that are inexcusable, especially in C++11. Why force programmers to figure out how to break cyclic references in their reference-counted smart pointers instead of just providing a real (but optional) garbage collected pointer type? Why force programmers to figure out how variables should be captured in a lexical closure instead of just always capturing by value (if you want capture by reference, why not just capture a reference by value?)? Why is there still no reliable way to report errors that occur in destructors? Why is error recovery still so problematic in C++, when other, older languages manage to provide useful facilities?
Most of C++'s problems are the result of the attempt to satisfy everyone's needs simultaneously. Rather than doing one thing well, C++ does many things poorly.
I agree with most of the points but this one seems suspect to me. This would break code like (forgive the possibly wrong C++11 syntax):
auto sum = 0;
std::for_each(my_vector.begin(), my_vector.end(), [](x){ sum += x; })
We actually made the mistake in Rust of making closures capture by reference or by value depending on what type of closure it is, which confuses newcomers immensely. It's scheduled to be fixed by making all closures capture by reference (and if you want to capture by value, use an object instead).I don't follow you. There's nothing inconsistent about allowing the closure representation to capture by reference. Indeed C++ allows this, if you write the appropriate capture clause.
http://www.cplusplus.com/reference/numeric/accumulate/
It is also worth pointing out that you can always thread state through accumulate/reduce/fold and achieve the effect of capturing references / having mutable objects in iteration constructs like for_each. That is how you see things being done in functional languages, and I find myself doing the same in Lisp with some regularity (even though you are generally dealing with references in Lisp; pure functions are usually more readable and less error-prone, at least in my experience).
In C++ there is another reason that capture-by-reference is a sensible default: there is no garbage collector. It is up to the programmer to ensure that the lexical environment remains valid for the lifetime of the closure, and so you can only capture locals by value if you return a closure. C++ programmers wind up having to capture smart pointer types by value in that situation anyway, which is basically what I said: if you want to capture by reference, create a reference (or pointer or smart pointer or whatever) and capture the value of the reference itself.
That's true, and it's why we applied the same reasoning to Rust. But we ended up with a very confusing situation. The definition of "closure" in an imperative language really implies capturing by reference; virtually all imperative languages that aren't C++ (or Rust) work this way (in Java it's a little muddy because of the final restriction, which is much maligned because it violates this intuition).
A closure that captures by value just isn't a closure by most programmers' definitions, and I think that the language would be better off treating it as something syntactically different from a closure.
auto sum = 0;
std::for_each(my_vector.begin(), my_vector.end(),
[&sum](unsigned x) { sum += x; });
I'm sure it could be golfed further though.E.g. the difference between references and pointers. E.g. the phenomenally baroque template syntax. E.g. phenomenally complex rules for multiple inheritance. Slicing problem. Syntax so hard to parse only a couple do it right. Lots of features that just don't carry their weight (operator overloading).
Operator overloading is crucial for things like graphics and bignums—basically, heavy math operations over anything that isn't a primitive.
I've programmed C++ for probably more than a decade now and I've been bitten by many of its features at some point, but operator overloading would rank right near the bottom of my burn list.
It can be abused, that's for sure, but so can many C features.
To avoid dismissing C++'s complexity too easily as simply the price of flexibility and performance, let's review what C++'s complexity looks like in practice. For example, read this:
http://simpleprogrammer.com/2012/12/01/why-c-is-not-back/
Memory stomping bugs; all the ways to initialize a variable of a primitive type; copy constructors; overloaded assignment operators; move semantics; smart pointers; value versus reference semantics; the list goes on. Against the cognitive load imposed by complete control over memory management, and lack of verifiable memory safety, JavaScript's warts seem quite minor to me. I imagine that many programmers who work in "managed" languages would agree.
But when we want to use value semantics, for whatever reason, the more complicated value copy is, to some extent, just intrinsically complicated. In java, Object.clone basically has all of the same problems -- you sort of know what you'd like it to do for any given object in any given context, but you've got to read code to figure out what is actually going to happen.
Because you will sacrifice performance over Java's GC. For maximum performance you really want precise garbage collection on both stack and heap, with generational concurrent operation and a two space copying collector in the nursery. Boehm can't provide this, because the language is not designed for GC.
This might be a dumb question but what run-times have a garbage collected stack? I Googled around but did not really find anything.
The reason I've stuck with C/C++ is because everything I've ever written still works. Meanwhile my friends are playing with new languages every year (and all the books, classes, and conferences to go along with them). All the new syntax's seemed driven by corporations vying for mindshare (money), not the betterment of our industry.
Give us a real alternative that is community driven and standardized. It needs to compile everywhere, have the option of a great IDE, and have libraries that make sense. We don't need another cute or toy language (JavaScript's 10 day incubation that I'm stuck living with now). Until then ... C++ is what we have.
With that said I'm looking forward to seeing progress on the language as I think we've been needing a systems programming language where you can express ownership semantics as part of the language itself, and have the compiler check those for you.
Changes to the language should not be done just to attract more users. This is especially true if these new users would be today's PHP, JavaScript and Ruby users. The C++ community is much better off without these kinds of programmers.
It's much better for people to use C++ when they realize that they need the powerful functionality it offers, rather than dumbing down C++ in a way that'll make it attractive to less-skilled developers.
Relevant anecdote: I know a number of C++ programmers who said they would use Haskell / Python / etc. to "prototype" a system, then switch to C++ when they needed what C++ has to offer. None of them made the switch back to C++ and their "prototypes" became finished products.
Keep in mind that there are also many cases that are the opposite of what you describe. I've worked with many systems that were initially implemented in a non-C++ high-level language, yet the developers had to go back and start incorporating C or C++ code at some point for various reasons, if not re-writing the entire system in C++.
The hybrid approach that Python allows for is extremely powerful, given how it makes calling down to C and C++ code from Python code quite trivial, or embedding a Python interpreter within existing C or C++ code.
This is because the vast majority of the time people's claims of performance requirements are complete BS. If you're writing true system software (an OS kernel, a database, an MQ system, etc) then shaving microseconds is going to matter because 10s, 100s, 1000s or even more applications built atop or using your code will be leveraging the minor gains you are creating. If you're building "Misc App X" then it's probably not going to matter.
Another thing I noticed is that of those people who do have serious performance requirements (OS kernel etc.), many choose C over C++. We all know what the most famous example is. I have no experience in this area. I wonder if there is any data on the use of C vs. C++ vs. other languages like Go in that realm.
in Masterminds of Programming, p.2 (Interview with Bjarne Stroustrup) O'Reilly Media, Inc. (2009)
A truly great read...
But Java or JavaScript? If your performance needs are satisfied by those languages, great, but you're not the target audience - not today, anyway, now that those languages exist and are reasonably fast. But if not, good luck trying to beat the JVM into doing what you want. [2]
I didn't think anyone uses Javascript unless they had to because the code needed to run in the browser.
C++ did not start out as complex language, though. Quite the contrary. Just take a look at early C++ books. The turning point happened in 1995 when Stroustrup switched the C++ paradigm from object-oriented to the functional inspired 'STL paradigm' (remember 'multi-paradigm'?). Afterwards a clique of 'Boosters' (nomen est omen) took over the language development and bloated C++ to the current mess. In evolutionary terms: C++, a versatile mammal that developed into a dinosaur.
That depends on what you think is impressive. A project that has clear and simple code is impressive, in any way needlessly complex and difficult to understand code is not.
Perhaps so, but those that do disagree might be worth listening to:
i ser following drawbacks of c++ - the default is very often not what you should do. this is the price for backwards compatibility this makes it hard for the novice but you get used to. - the incredible long build times esp when working with modern libs. how many hours did i spend waiting for the compiler and how many for optimizing ny precompiled headers. this is really a produxtivity killer. its has also historical reasons but i dont understand why no proper module system gets depeloped - the lack of support for runtime errors. for 7 years i have not seen a crashing java program without giving me a hint where it crashed. and actually i have seen far less total crashes. in c++ you do one small thing wrong and the whole program ends with an abnormal program termination and you dont have any clue what happened.