Thursday 25 May 2017

Programming language rant

Brian Bucklew, the developer of Caves of Qud (highest rated turn-based tactics game on Steam) and Sproggiwood (a casual roguelike very good both for beginners and for experts), has recently been involved in an external C++ project, and has tweeted a long series of rants about his hate of C++ from the point of view of a C# developer. Since Twitter is not a good medium for discussion, I am posting my thoughts on the subject here.

I will start with mentioning that I have no experience with C#. I have experience with Java. I consider C# to be an evolution to Java. Since I was not satisfied with Java, I am not interested in trying C#, since I believe it to be based on the same general rules. One reason why I did not like Java was too much focus on object oriented programming. The widely held belief is that it is good to learn as many programming paradigms as possible, as then you are able to use the good things about these paradigms in whatever programming language you end up working with. I consider OOP to be one such concept: something that generally every programmer should learn (ideally on a fully OOP language such as Smalltalk) but not something that everything has to be written in. I consider OOP to be a very useful representation of polymorphism. For example, in Hydra Slayer, there are multiple kinds of objects: hydras, consumables, and weapons. I can list all the objects in sight of the player, and then e.g. list their names. It is useful to call a name() method that will work correctly for all kinds of objects despite their different inner representations. But not everything has to be an object. This was the only use of OOP in Hydra Slayer (not including NotEye which does use OOP quite significantly), and HyperRogue did not use polymorphism at all until recently.

Oh my fucking god I have to make two fucking separate files for every fucking object. How can anyone like this language. C++, two pluses because you have twice as many files as the precise same thing architected in any other language. I really think C++ programmers are blind to how many incredibly annoying things you waste your time doing in C++. Ugh. It's so damn wordy.

This actually looks very similar to the issue I had with Java. Having to create a file for every single object; not only that, also a tree structure is forced. Furthermore, because of the lack of conditional compilation, I could not create a single version of the HyperRogue Android source that would compile as one of three packages depending on the compiler options chosen, and I had to create three copies of this source (this was done with a script, but still); maybe an experienced Java programmer would do it in some better way. I do not know to what extent C# fixes this. It does have conditional compilation, at least.

I hate having two separate files (.c and .h) too. But contrary to Java, the language does not force you to actually do this. In my biggest project (HyperRogue) the main file hyper.cpp includes init.cpp, which includes all the other cpp files in turn, from the most fundamental ones. When I want to call some function which was not yet compiled, I simply copy the declaration to hyper.h which is included first. But this comes up very rarely and it is far from a problem that Brian makes it to be.

It takes 10 seconds to recompile HyperRogue. Not ideal, but not bad either. If header files are necessary, generating them automatically does not seem to be a difficult task.

Like C++ guys think they have the ultimate concise low level language, but you basically have to write everything in two representations.

I consider the Google Code Jam statistics to be a good benchmark of conciseness. Contestants have to implement solution to algorithmic problems as fast as possible. It has to run fast, which hurts Python, but not languages such as C# or Java. Most contestants decide to use C++. From contestants who have passed to Round 2 (thus, no newbies), ones using C++ are also more likely to advance to Round 3 than ones using Java or C#.

Want to setup a map<> for something? Make sure you keep track of everything and/or iterate it when erasing so you can delete everything.

I do not understand what Brian is referring to here. Deleting every pointer in a map is simply for(auto& p: m) delete p.second;. In most cases, you do not even need that! I usually do maps of ints, pairs, strings, vectors, non-polymorphic types, etc. which do not require anything; and contrary to garbage collection, RAII will take care of cases which are more general than simply clearing the memory. If polymorphism is required, smart pointers will handle deletion.

bahahaha I forgot you can do multiple inheritance in C++ . . . . what a bad idea

Maybe nobody uses it, maybe people use it to implement something like Java interfaces (which were designed later), maybe it is used in the implementation of C++ itself. I do not care. I never use this and I have no problems with this feature existing. I consider the diamond problem to be an inherent to (class-based) OOP, especially in roguelike programming, when the hierarchical subclass distinctions are often very blurry (attack monsters with a cockatrice corpse? uh, it was first a monster, then it was food, and now it is a weapon?).

omg I forgot C++ can't compile circular references; so a child can't simply refer to it's parent without some sort of hackery like void* (I've written several million lines production of C++ in my life, for those concerned I have no clue what's going on with C++)

Simply use forward declarations, I do not get how Brian did not remember this.

Wow, c++ got a little unhinged with copy/move semantics since I last visited it, hu? I'm trying to imagine people with like less than 20 years of C++ experience writing correct C++ programs nowadays..."

Yeah, the copy/move semantics sounds crazy. But... what happens when you compile an older program with C++11? It compiles and works faster, because move semantics work behind the scenes. One only needs to understand it and care about it when writing extremely efficient programs.

...also not having introspection/reflection sucks a fucking A+ hairy nutsack.

For introspection (as in, checking what is the exact type of a polymorphic object) dynamic_cast is fine. As for reflection -- this seems to cure a problem that I do not have because I rarely use OOP. There is OOP in NotEye, and debug/save/load functions are case-by-case listings of all fields -- this is a place where reflection could be used, but I do not believe it would help that much because (a) this was trivial to write, (b) it appears easier to handle special cases, such as when new fields are added / class hierarchy is redesigned and the load function has to be adapted without breaking older saves. (Anyway, I have managed to implement a method executing a given lambda function with parameters (auto field, string name) for all fields of a polymorphic object in seven lines of C++ plus two lines per class, and creating objects by class ID in about ten extra lines; despite having to write a list of fields and subclasses for each class, I like my solution for efficiency and type safety.)

Coming back to C++ after such a long layoff it's amusingly clear just how much Stockholm Syndrome you develop with its language design.

There were several tweets like this. I know some legitimate criticisms of C++, but all Brian's criticisms appear to be caused by some kind of bias -- problems already solved appear easy, new problems appear difficult, or maybe self-confirmation, or working on some very poorly designed external project. It is hard to see what could be achieved by using templates, macros, RAII, and smart pointers when being used to OOP and reflection. Rather than accusing people of "Stockholm Syndrome", I suppose that some people think in this way, and other people think in that way, and everyone chooses the language which is better aligned with their way of thinking.