Hacker News new | ask | show | jobs
by PommeDeTerre 4689 days ago
C++, Java and JavaScript each have their own different kinds of complexities.

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.

4 comments

C++'s complexity does not arise from its power. With the exception of templates, C is just as powerful but far more orthogonal and simple.

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).

> 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.

Its a trick pony. I don't think that use is enough to carry its weight. Also, abusing it immediately in the streams library just encouraged people to find elaborate nonsensical uses for it.
Operator overloading is very useful in the context of generic code though (through templates).

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.

You seem quick to denigrate JavaScript, and programmers who willingly choose it, wherever you can. But with regard to the kind of complexity the OP discusses, which roughly translates to added cognitive load, it's not at all clear that JavaScript is worse than C++ -- and I have real experience with both.

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.

Author of blog post here: Thanks for your lucid remarks. JavaScript certainly has its share of annoyances, but likening those to the complexity issues of C++ seems inappropriate to me.
"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"

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.

> 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?)?

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).
Hair-splitting point: your example is not very good, because for_each is the wrong way to do what you are trying to do. The STL already has the right way to do it:

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.

> 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.

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.

This is the working code (you have to declare your intention to capture according to the g++ and clang++ error output, and x requires a type in the parameter list):

    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.
That code is wrong. Its obvious why its wrong when you think about the closure representation. Forcing a developer to understand the implementation is not inconsistent with the C++ mentality (see: everything having to do with inheritance and parameter passing).
> That code is wrong. Its obvious why its wrong when you think about the closure representation.

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.

Author of blog post here: I believe JavaScript is another example where the problems (not sure if I'd call it complexity, but problems they are) can be understood by looking at the history of the language. Brendan Eich himself has said: "I created JavaScript in 10 days in May 1995, under duress and conflicting management imperatives."
True, that could very well explain its horrid state in mid-1995. But that was 18 years ago, and things really haven't gotten any better since then.

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.

Template metaprogramming is the reason I have effectively given up on C++.
Modern C++ is worse than classic C++. They threw shit on top of the dung pile.