10 reasons to love C++
In the past few years, I’ve done most of my game development in Java. It didn’t use to be that way. Before Android and libGDX came along, when C++11 was still C++0x, I used C++ almost exclusively. And recently, because of some performance-critical bits in Mystery Game No. 1, I got to use C++ again. And I loved it!
One of the foundational principles in C++ is “pay for what you use”. This implies that, as a programmer, you can have a fairly clear idea of how your source code will be translated into machine code, and how memory will be laid out. And yet, despite this seemingly leaky abstraction, C++ still lets you write high-level code that (once you get used to syntactical oddities) is really quite readable.
Below are 10 things I gathered during the past two weeks that I really loved about the language. Most of them I’ll contrast with Java, but there are plenty of other languages that would also look bad in comparison.
1. Typedefs
Yes, the humble typedef
, hardly changed since its C days:
typedef float Distance;
Now I can write Distance x
to remind myself what kind of number x
really is, so I reduce the risk of accidentally adding a temperature to a distance.
There is another advantage: if I decide that all my distances should be represented in double precision, I just have to swap out float
for double
and Bob’s your uncle. In Java, I would have no choice but to go through all places where float
is used, decide whether it represents a distance, and swap it out if so. Ouch…
However, typedefs are just aliases. They don’t actually prevent me from adding temperatures to distances. Which brings me to the next point…
2. Cheap wrappers for primitive types
My C++ teacher used to say: “do as the ints do”. Which means, to the extent possible, that any type you define should behave in the same way as a primitive type. In Java, this is very much impossible. The primitives are special: they are value types whereas everything else is reference types, and so they can be allocated on the stack while everything else must be allocated on the heap (although the Java abstraction doesn’t make this distinction explicit).
In C++, however, we can create a wrapper easily:
class Distance {
private:
float meters;
}
Now, Distance
really is a new type, and cannot accidentally be mixed up with a temperature. And to do as the ints do, we can add all the expected operators: addition (Distance + Distance = Distance
), multiplication (Distance * Distance = Area
), division (Distance / Time = Speed
), and so on. We can even add custom methods, like toInches()
. And still, Distance
would look and feel a lot like a float
.
This is a great abstraction, but at what cost? Of course, the cost of having to write and maintain it. But nothing else! I can be sure that this Distance
class will compile down to the exact same code as doing all the operations on floats directly, and that each class instance will occupy only 4 bytes in memory (padding aside). The runtime cost of this abstraction is zero. Eat that, Java.
3. Stack allocation
Traditionally, one of the pain points of C++ is memory management. Since there is no garbage collection, you have to take care to destroy your objects yourself when you stop using them – and not to use them after you’ve destroyed them.
However, the language does give you the tools to deal with this. In the code I wrote recently, I only wrote new
once or twice. The rest of my objects are all local to functions, so they live on the stack, “as the ints do”. Some of those (like vector
) may actually do allocations behind the scenes, but that’s all fully abstracted away and I don’t need to worry about it. So memory management hassles are minimal: every object’s lifetime is managed by its scope. And stack allocation is super cheap, because it doesn’t require any memory management.
Contrast this to Java, where every non-primitive has to be allocated via new
, thus generating garbage later on, which the garbage collector has to deal with. The GC on Android was pretty bad for a long time, sometimes stopping the entire program for tens or hundreds of milliseconds while it did its thing. This is better in recent Android versions, but we’re at the mercy of mobile carries to push the updates, which they’re notoriously lazy about. So for games, we still can’t rely on the GC, and we have to make sure not to create any garbage. And without the ability to allocate Java objects on the stack, this gets tedious.
I’ve even heard similar advice about garbage avoidance for Unity, which is based on C#, so this is by no means unique to mobile. Although at least C# lets you create struct
types, which are value types and so can live on the stack.
4. Functional programming
With the introduction of lambdas in C++11, the algorithms in the standard library suddenly became a lot more usable. Once you get used to the weird syntax, this lets you write really expressive, compact code:
string text = "hello world";
string uppercase;
transform(text.begin(), text.end(), back_inserter(uppercase),
[] (char c) { return toupper(c); });
The great thing about these algorithms is that they are container-agnostic. They work equally well on a vector
, a list
, a string
or some weird type that I defined myself. As long as it supplies suitable iterators, the standard algorithms can deal with it. And thanks to the magic of templates, a specific version of the algorithm is compiled for each type of container, so no runtime indirection is needed.
In Java, on the other hand, functions don’t even exist as first-class citizens. Anyhting like the above would need the creation of a temporary anonymous class which implements some particular interface. Five lines of code, of which only a few characters actually matter. The good news is that Java 8 finally has support for lambdas, but it cannot be used on Android (yet?).
5. Const methods
Some people apparently hate it, but const
is my best friend. I particularly like const
methods, which are guaranteed by the compiler not to modify the state of the object. This gives an ironclad guarantee that these methods are always “safe” to call, so they help in designing clean interfaces.
But the real usefulness of const methods only begins to show in the next point…
6. Const references
Did I mention I like const
? Another great use of this versatile keyword is when declaring const
references:
void estimatePrice(const Car& car) { ... }
What this says is: car
is a reference to a const Car
. And a const Car
, of course, cannot be modified. That means I can safely pass a reference to my expensive Ferrari into this method, and I have an ironclad guarantee that it won’t be changed in any way!
This is incredibly useful, because it eliminates the need for many defensive copies. In Java, if you pass something to a method, you can never be certain that the method won’t alter it, so it is often wise to pass a copy instead of the original. And making copies, of course, is expensive.
The ability to mark anything const
also does away with the need for immutable types. It’s often most natural in C++ to make types mutable (again, as the ints are), and use const
to mark them immutable when the situation calls for it. The Builder pattern (used to construct more complex immutable objects) is something you rarely need in C++.
7. Templates
Superficially, templates look like generics offered by languages like Java and C#. But the angle brackets syntax is misleading; although templates can be used to define generic containers, they are a very different (and much more powerful) kind of beast.
For example, you can have template specializations. This is used in the standard library to ensure that a vector<bool>
only takes up 1 bit per entry (rather than the 1 byte usually taken by a bool).
Furthermore, templates allow a kind of static duck typing: pass me a type that has a method quack()
, and I can work with it. Note that, unlike in Java or C#, the type does not have to implement a particular Quacking
interface; just having the right methods is sufficient. And if it doesn’t, the compiler will tell you (albeit in an excessively verbose way).
And because every template specialization is compiled separately, all of this is again completely free of any runtime cost (except maybe some code bloat, which is rarely a problem).
8. Operator overloading
Again something that Java doesn’t support: operator overloading. So we are forced to write
Vector2 sum = a.add(b);
instead of the far more natural
Vector2 sum = a + b;
But C++’s operator overloading goes a bit beyond the obvious arithmetic and comparison operators. You can overload the dereference operators, *
and ->
, which lets you create your own types that look and behave just like pointers. You can overload the assignment operator, =
, which lets you create a string type that can be assigned directly from a char[]
. You can overload the index operator, []
, which lets you create array-like and map-like types. And finally, you can overload the function call operator, ()
, which lets you create types that are callable and behave like functions. The possibilities are endless, and when wielded properly, can lead to very expressive yet efficient code.
9. Default constructors, default assignment operators
If your class meets particular criteria, and you don’t explicitly tell it otherwise, C++ generates a copy constructor and assignment operator on your behalf, which just copy all fields one by one. This makes it a bit easier to “do as the ints do”, without the tedium of having to write these trivial methods yourself. (Sadly, a default ==
operator does not get created.)
10. Inlining
Any function can be marked inline
. This is a hint to the compiler that the function implementation should be inlined in the place(s) where it’s called, saving the overhead of a function call. For really small methods (like getters and operators), this can give a significant speed boost. Compilers should nowadays mostly figure this out by themselves, and indeed Java’s just-in-time compiler will also inline methods when possible and benificial. Still, for peace of mind it’s nice that you can give the compiler a nudge.
Counterpoint
To avoid coming off like the C++ fanboy that I might be, let me offer some counterpoints.
The history of C++ goes way back to the early 70s, and it shows. For example, the compiler still processes your source in one linear pass, top to bottom, so you have to make sure to declare your things in the right order and get your header files straight. In fact, the very need for header files is an artifact of the way C compilers in the 70s worked…
The syntax can be verbose and repetitive at times, such as when defining class methods out-of-line.
Often, no default initialization is happening, meaning that you have no idea what number this program will output:
int x;
cout << x;
It may often be 0, especially when your program just started up and your memory pages are still pristine, which makes such bugs very hard to detect (valgrind helps).
Still, all taken together, I like this language. I like how it gives me the tools to think and code at a high level of abstraction, yet lets me get really close to the machine when I want to. This is a rare combination of functionality that no other programming language can match, and I think it’s the prime reason why C++ is still so popular.