C++ for Java Developers

Why C++?

I come from a Java background. I LOVE Java. It's simple, it's easy to read, and it's inherently cross-platform.

Unfortunately for me, the preferred language of the game development industry is still C++ due to its speed. There are, of course, some notable exceptions, such as Minecraft (but even that received a C++ port to get it running on consoles).

Minecraft screenshot

Of course, for indie games like Abacus, Java is perfectly fine, but modern AAA games typically need to squeeze every ounce of performance they can get from the hardware, and random frame drops due to sporadic garbage collection just won't cut it.

So, when I started work on Open-Rival, I decided to write it in C++ as a learning experience. While it has resulted in a slower, more gruelling development process, it has taught me a lot.

What makes C++ so horrible challenging?

While it is broadly true that C++ is faster than Java, it is also a LOT more complicated. More than once, I've thought I was getting to grips with the language, only to discover that I'd misunderstood something basic. One Redditor described a similar phenomenon:

I think one thing worth mentioning is that C++ doesn't ever seem to end. In the sense that it's one of those things where the more things you learn, the more things you see that you still have left to learn.

Complicated Syntax

The C++ syntax is vast and complicated. There are many obscure keywords and operators, and their ordering and precedence can be confusing and counter-intuitive.

For example, the following is valid C++:

const int*const do_something(const int*const&)const;

Confusing Documentation

The documentation for C++ is not an easy read.

Get a load of this explanation of an implicitly-declared constructor:

If the implicitly-declared default constructor is not defined as deleted, it is defined (that is, a function body is generated and compiled) by the compiler if odr-used, and it has the same effect as a user-defined constructor with empty body and empty initializer list. That is, it calls the default constructors of the bases and of the non-static members of this class. If this satisfies the requirements of a constexpr constructor, the generated constructor is constexpr. Class types with an empty user-provided constructor may get treated differently than those with an implicitly-defined or defaulted default constructor during value initialization.

Lack of Clear Standards

While there are standards, the language has a complex history and is constantly evolving, often resulting in many ways to achieve the same thing and copious conflicting opinions.

Notably, Google have their own style guide, in contrast to the C++ Core Guidelines, which further divides the community.

Platform-Dependent Behaviour

Many standard library functions, and even the basic data types, can vary wildly between different systems and compilers.

Cryptic Error Messages

Often subtle errors - even basic syntax errors - can lead to very cryptic error messages at compile time, and runtime errors can be similarly unintuitive. Unlike in Java, where every error message results in a stack trace pinpointing its exact location, errors in C++ can often emerge from deep within a library, making the cause extremely difficult to pin down without detailed program logs.

For example, today I encountered this error:

C2678 binary '<': no operator found which takes a left-hand operand of type 'const Image' (or there is no acceptable conversion)

The root cause of this error was that I tried to use my Image class as the key to a map. Apparently, map keys must be comparable due to the way they are stored internally. Nothing in this error message even mentioned maps.

Debugging becomes even harder on a release build, as all debugging information is stripped out for optimisation reasons.

Poor Tooling

A 2018 poll showed that the most popular IDE for C++ development is Microsoft Visual Studio, by a significant margin. Unlike its slick modern counterpart, VS Code, which has taken the development world by storm, Visual Studio appears clunky and dated.

Visual Studio screenshot VS Code screenshot

Drag the slider to compare IDEs (Left: Visual Studio, Right: VS Code)

Simple functionality such as format on save and vertical rulers can only be achieved via extensions. What's more, Visual Studio did not have a free version available until 2013, which made it very difficult for newcomers to get started with C++.

The IDE is just one of many tools in a typical developer's arsenal. For any large project, linting can be invaluable to ensure consistency and eliminate bugs. Unlike other languages (e.g. tslint for TypeScript), in C++ there is no go-to tool for static code analysis. The general consensus seems to be to "use every tool you can get your hands on".

Put simply, the language is just too complex for a single tool to be sufficient.

Tips for C++ Beginners

If, despite my warnings, you still want to learn C++, then here are some tips that I wish someone had told me when I was starting out.

Get the right tools

Visual Studio is fine, but supplementing it with a few extensions can make a world of difference.

I highly recommend the following:

Everything is passed by value

In Java, passing an object into a method actually passes a reference to that object. In C++, by default it creates a copy of that object, by calling the object's copy constructor. It is important to be aware of this, because if you're not careful it can lead to all sorts of obscure bugs and performance issues.

To emulate Java's behaviour, we have to explicitly declare that a method will receive a reference:

// This one will create a copy of our Shape public void fill(Shape shape) { ... } // This one will NOT create a copy of our Shape public void fill(Shape& shape) { ... }

When passing references, be careful that the object does not go out of scope while a reference to it is held elsewhere! In many cases, this is the result of poor design choices, but if this is a legitimate concern, Smart Pointers may be the solution.

Resource acquisition is initialization

RAII is a common idiom in C++, but what does this mean in practice?

Forget about new and delete

Being a superset of C, C++ supports the keywords new and delete - but that does not mean they should be used. Using the new keyword automatically allocates memory on the heap, which is never freed unless delete is called - even if the object goes out of scope.

Instead, objects should be created by calling their constructors. This allocates objects on the stack, which means that they are automatically deleted (and their destructor is called) as soon as they go out of scope.

Objects cannot be null

Unlike in Java, where the default value for an object is null, in C++, an uninitialised object will cause a compilation error.

For example, imagine the following class:

class House { private: Chimney chimney; int numWindows; }

In Java, we could create a new House, and the chimney field would be null. Attempting the same thing in C++ would result in a compilation error, because we have not initialised chimney in the House constructor.

Summary

C++ is a language with seemingly endless complexity, but it can be tamed. Start simple, compile often, and think very carefully about what the code is doing; it's easy to make mistakes, and often the compiler will be quite happy to let you do something stupid.

Finally, the folks over at the Code Review Stack Exchange can be very helpful once you have some code to show off.

Good luck - you may need it!