C++ and Thoughts On Java, Go, and Rust

Historically most programming situations that called for high performance, particularly in the field of systems programming, would have been written in C or C++. That has changed a lot over the last twenty five years or so. Java has increasingly been the programming language of choice for high performance server applications. Recently Go and Rust have made inroads as high performance alternatives to Java that claim to offer similar or better performance. Yet I still find that I really love writing C++, and at least for situations where speed or memory utilization are important I prefer writing C++ to any other language. In this post I will explore some of the aspects of C++ that I find appealing, and why Java, Go, and Rust don't make the cut for me.

Why Not C?

I'll start with the easiest question that I am frequently asked when the topic of C++ comes up, which is why I don't like writing C. Actually I don't mind writing C, particularly for smaller programs. Except for a few mostly unimportant corner cases you can think of C as a subset of C++, and therefore a lot of the things I like about C++ also apply to C.

Given the choice though, I do prefer C++. The most important reason for this preference is that C++ has much simpler error handling than C. A commonly occurring pattern in all programming languages is the need to clean up resources after an error has occurred. In languages with garbage collection features this will usually be handled by the garbage collector. C and C++ do not have garbage collectors, but C++ does have an elegant solution to this problem by way of object destructors. Resources that must be freed on error are typically wrapped in C++ by a class or struct that implements a destructor freeing the resource; this pattern is called RAII. The RAII pattern ensures that you can return out of a function at any point and any classes allocated on the stack prior to the return will have their destructors run immediately. This is simply not possible in C. Instead, C encourages the use of goto statements for error handling. In general I find that these statements make the flow of execution harder to understand, especially if a function allocates more than one or two resources. The goto facility also requires that the programmer remembers to use them in every situation where a resource is allocated, whereas C++ object destructors are always run automatically leading to fewer chances to make mistakes.

Another important reason I prefer C++ to C is that C++ offers a much more complete standard library, particularly with respect to high performance data structures. The C standard library does not really give you any data structures at all. If you want to use things like dynamically sized vectors or hash tables you either need to implement them yourself, or you need to pull in a nonstandard third party library like GLib. As just discussed, these data structure libraries require the programmer to do a lot of manual error handling to ensure that resources are freed in all cases.

Most C data structure libraries make extensive use of preprocessor macros and/or function pointers to implement polymorphism. Both of these approaches have serious limitations. Preprocessor macros operate on a textual level and therefore can easily lead to surprising bugs and compiler errors if used incorrectly. Many text editors and IDEs have difficulty understanding macros for a few inescapable reasons. For instance, it's common to specify preprocessor defines as compilation flags, meaning that if your editor isn't tightly integrated with your build system it has no way to actually understand how macros will be expanded. Function pointers have a different set of problems. For one, the syntax for using function pointers is widely regarded as confusing. The other problem with function pointers is that they usually cause the program to have extra indirection for function invocation because they require an extra pointer lookup. Function pointers also prevent inlining, an important compiler optimization. For many data structure applications this causes significant performance overhead compared to C++ data structures implemented using templates. Some C programs try to work around this problem by way of extensive code generation which allows the compiler to inline functions, but doing this effectively with the cpp preprocessor is either impossible or incredibly verbose and error prone.

Due to these data structure problems, most C++ programs using the STL are faster than C programs using equivalent data structures. Furthermore templates offer a lot of safety features not available to C programs using macros/function pointers. It's always possible to write C code that is as fast as C++, but frequently doing so requires either manually inlining things or the use of novel code generation tools.

The final reason that I prefer C++ to C is that in cases where one does want to call into C code, it's incredibly easy to do so from C++. All major C libraries that I can think of are annotated to allow use from C++ code. Typically this just requires a small amount of boilerplate at the top and bottom of the header file, like this:

#ifdef __cplusplus
extern "C" {
#endif

/* C code goes here */

#ifdef __cplusplus
}
#endif

What this does is ensure that the C++ compiler won't attempt to do name mangling for code in the extern "C" block. This not only allows C++ code to include these C libraries, it can also be used to write C wrappers to C++ code. The ease of calling C code from C++ means that there aren't generally features or libraries that would be available to you in C but not available from C++; and if you do find such a library, it's trivial to modify it to be compatible with C++.

Why C++

C++ has a number of unique features that aren't available in competing languages like Java, Go, and Rust. Some of the features I'll outline here are available in some of these environments, but none of these languages offer all of these features.

The first feature that I really like is C++ templates. This is a widely reviled language feature, and indeed when overused templates can cause a lot of problems, particularly with respect to understandable code and compiler error messages. That being said, C++ templates are an extremely powerful metaprogramming capability, and must more powerful than C macros. I'd rather be given enough rope to hang myself than not have it at all. This is also my main objection to Go: Go has no serious generic metaprogramming capabilities unless you count go generate (which is arguably a lot more confusing than C++ templates). The ability to do polymorphic metaprogramming is an essential part of DRY. As a programmer there's nothing I hate more than writing tedious boilerplate that a compiler could implement automatically (and more succintly than I could). Templates are an extremely powerful way to write short, correct, and fast programs.

Another feature that I've really come to appreciate recently is first class support for assembly language. The __asm__ keyword is reserved in both C and C++ explicitly to allow compilers to provide the ability to inline assembly code. This is a relatively unique feature. Of the alternative languages I mentioned, only Rust supports inline assembly. One should be judicious when using inline assembly, but in cases where it comes up it is absolutely indispensable.

Aside from supporting inline assembly, all major C and C++ debuggers have extremely high quality support for inspecting generated assembly and stepping through assembly. This is a critical feature for understanding how code is optimized and for debugging certain situations. For instance, if you want to understand how indirect jumps work, what's happening in LTO code, or how inlined code works you need to be able to inspect and step through the generated assembly. Once again Rust is the only alternative language out there that really provides first class support for this, in Rust's case by piggy backing off of GDB/LLDB, the GNU and LLVM C/C++ debuggers.

C++ doesn't have an included runtime, by which I specifically mean that there's no garbage collection that will interfere with your optimized code. This is a big deal for a couple of reasons. The first is that for high performance code garbage collectors can easily get in the way much more than they help. It's not uncommon to hear of Java programmers doing things like majorly refactoring their code to make use of things like object pools to try to reduce the frequency with which the garbage collector runs. While this can be an effective technique at mitigating GC pauses, it's an example of a situation where the "feature" of garbage collection causes developers to try to actively work around their programming environment. I content that this is especially bad because it causes developers to express application logic in unintuitive ways.

The other major problem with garbage collected languages is that they have serious problems dealing with large heap sizes. This is particularly a problem for databases and database-like applications. By database here I mean any program that has a working data set larger than a few gigabytes, and in particular any program where the data size might exceed the amount of memory on a machine. If you try to actually map all of this memory into a runtime like Java the garbage collector will have huge problems with latency (or throughput) as it scans the heap for unreferenced objects. A common way that programs will work around this is by using a structured on-disk format and then using regular disk I/O to access the data. This can be somewhat effective because the kernel page cache will cache recently accessed data in memory. The page cache, however, is always less effective than mapping data into RSS memory because accessing data via the page cache requires extra system calls to be made. This is precisely the reason that high performance database engines like InnoDB (which is written in C++!) map this type of data into userspace memory and avoid using the page cache.

One other problem I've noticed with modern C++ alternatives is that they often have poor support for advanced operating system primitives. For instance, if we continue considering the use case of implementing a database, the Linux kernel offers a number of advanced features for optimizing I/O. Most people should be familiar with the system calls read(2), write(2), and lseek(2). These are the basic foundations of doing disk I/O. There are some less well known alternatives to these that are designed to optimize the number of context switches applications need to make. For instance, the system calls pread(2) and pwrite(2) allow applications to coalesce read/seek and write/seek into one system call in each case respectively. Likewise, Linux offers "vectorized I/O" capabilities via the system calls readv(2) and writev(2) which allow a read or write system call to specify the input/output data as a set of multiple buffers which allows the application to avoid doing extra memory copies (or extra system calls). So far the system calls I've mentioned are not too well known, but critical for writing high performance applications. Linux even goes so far as to implement the baroque system calls preadv(2) and pwritev(2) which allow combining read/write operations with seeking and vectorized I/O, all in a single system call! This is extra fast but also extra unportable; for instance, OS X does not implement preadv(2) or pwritev(2). And all of these things go out the window when trying to target Windows which has a totally different set of system calls for doing these types of operations. Therefore a lot of these system calls are unavailable in runtimes that try to offer portability as a major selling point. As far as I can tell you can't invoke preadv(2) or pwritev(2) from Java or Go (but you can in Rust if you don't mind calling into unsafe code). Java and Go do implement pread(2) and pwrite(2) but only if you use special interfaces; for instance, with Java you must use java.nio to use pread(2). These languages at a fundamental disadvantage for implementing high performance database applications because the high performance system calls are either unavailable or difficult to use.

I've used some of the advanced I/O capabilities in Linux as an example here, but there are simpler examples that fall apart too. Try calling fork(2), a Unix system call coming up on 50 years of age, on Go, Rust, or Java. None of these languages support it because forking a process requires careful handling of file descriptors that survive the fork. As far as I know none of these language support fork specifically because they internally use evented I/O loops and no one knows how to expose this in a sane way to application developers. This is unfortunate because one of the main use cases for forking is forking a process early in its life cycle before there even are a lot of file descriptors to worry about, for instance to create a daemon process. You might also run into problems forking a complex C++ application that has a lot of open file descriptors, but at least you have the option to handle the matter, which you don't have at all in other environments.

Why Not Rust

From the analysis I've presented so far it should be somewhat apparent that of the alternative languages to C++, Rust is the one that offers the most features I enjoy from C++. And indeed, I do think that Rust is pretty cool, and it's a lot more palatable to me than Go or Java. Rust is definitely more targeted towards C++ programmers than either Java or Go, and that is borne from its history as an attempt to develop a real world alternative to C++ for use at Mozilla.

The main gripe I have with Rust is that while it does have a lot of really powerful features, it's unclear to me what the major advantages it's trying to offer over C++. For instance, one of the major language features touted in Rust is pointer ownership with move semantics which is a powerful way to avoid memory leaks via static verification that memory is freed, and with no runtime overhead. This is indeed a cool feature. It's also exactly equivalent to a C++ std::unique_ptr. Therefore if you want to write code that statically verifies correctness with respect to releasing allocated memory and with no runtime overhead you can just as easily use C++ as you can use Rust.

In fact, if you've been following the recent language developments in C++ (e.g. the recent C++11 and C++14 language standards) you'll find that many of the advanced Rust language features were added to C++ before or at around the same time as their introduction in Rust. Unique pointers are one example of this, but it also applies in other languages such as builtin concurrency primitves and move semantics. There are some unique things about Rust such as a lack of null pointers, but even that guarantee goes out the window if you need to call unsafe code, which you will if you are doing advanced system programming. If you've kept abreast of the latest C++ developments you might appreciate the simplicity of Rust compared to C++, but it's hard to be impressed by the actual language features.

I'm going to keep my eye on Rust because I do think that it shows a lot of promise, but for the time being the language features it offers over C++ isn't enough in my opinion to give up the tight integration C++ has with C and the native debugging support that GDB offers with C++.

Conclusions

The number of C++ programmers out there is probably diminishing, and certainly the core group of C++ developers in the wild is getting older. This is a direct consequence of the fact that the space historically occupied by C++ is being crowded by other high performance languages. That being said, I don't see C++ going anywhere anytime soon. C++ has been extremely successful at incorporating new features and modernizing itself over the past few years. C++ is also unique in its facilities for tight interoperation with C and assembly.

If there's any unifying principle in the C++ language specification and the way the language has been evolved, it's been to make C++ fast. A lot of the aspects of C++ that people dislike are areas where the language has chosen to give more options to allow compilers to generate fast code at the expense of making the language more complicated or less understandable. It's natural that many developers will feel like that's the wrong tradeoff: that developers spend more time trying to understand and debug programs than they spend trying to eke out every last bit of performance. That's definitely true in many applications, but the fact also remains that there are a lot of specialized situations where performance is paramount. In these situations it's hard to beat C++.