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

IDEs

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)

The difference may not be obvious at first glance, but 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++.

Static Code Analysis

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:

  • ClangFormat

    This can automatically format your code according to your preferences. It is highly configurable, and once set up, guarantees consistency across the codebase, and frees up developer time to focus on more important issues (like where that cryptic error message is coming from!).

    clang-format.exe can be downloaded as part of LLVM. Visual Studio can then be configured to use this executable for its formatting (the version of ClangFormat that ships with the IDE is outdated).

  • Format on Save

    This combines nicely with ClangFormat to ensure that your code always remains correctly formatted, with no manual action required.

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?

Avoid using new and delete

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. This can lead to memory leaks.

If we omit the new keyword when creating an object, it is created on the stack instead. This means that the lifetime of the object is automatically tied to its scope; when the variable goes out of scope, its destructor will be called, which helps to avoid memory leaks.

If heap memory is required (e.g. for large or long-lived objects), smart pointers can help to ensure they are cleaned up correctly.

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 (assuming the Chimney class cannot be default-constructed).

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!

Published 2020/08/02

Last updated 2023/10/19