Chapter 3.4

ES.50: Don’t cast away const

Story time

When asked about my favorite C++ features, besides deterministic destruction, const always makes the list. It allows me to divide an interface between view and control, giving users a head start in learning how to use it. This separation has come to my rescue countless times, but never more starkly than in my first big game project.

At the turn of the century, my employers started a mighty work, a computer game called Rome: Total War. This was a real-time strategy game set in the era of the Roman Empire. The game featured field battles with hundreds of soldiers operating in cavalry, infantry, and artillery units, along with the extensive control options nec-essary to navigate them around the battlefield. You could arrange individual units into formations, group units together, send them into battle against enemy forces controlled by a cunning AI opponent, and watch the entire drama unfold in beauti-fully rendered, glorious 3D.

It turned out to be quite a lot of work, rather more than we expected. One reason for this was the desire to make it a multiplayer game. Rather than going into bat-tle against an AI opponent, we wanted to offer the opportunity to compete against another human player. This presented a considerable set of problems, not least of which was how to maintain the same world state on both machines.

This is a problem throughout multiplayer games. If you are playing a motor rac-ing game, it is important that all cars appear to be in the same position on all players’ machines. If two different cars cross the finish line first, the game is spoiled. One way of overcoming this is to designate one machine as the authoritative world server, ensure all the client machines send regular updates about their instance of the game to this server, have the server resolve these updates into a new world state, and then send the new state back to the client machines.

In the case of a motor race, that would mean sending, say, 20 new car positions and accelerations back to the client machines. This is no great burden: only an x and y component are needed (unless your racetrack features bridges), so that resolves to four components for 20 cars at four bytes per component for float types, which is 320 bytes per world update.

Back then, 30 frames per second was an entirely acceptable frame rate, but you did not need to update the world for every frame, merely the view of the world. Your model of the world includes the positions and accelerations of each vehicle, so you can make a reliable estimation where each one is going to be thanks to Newton’s equations of motion. All you need are world updates that are faster than the human brain can recognize. Ten hertz is enough, which means the server must broadcast 3,200 bytes per second to each of its clients. Squeezing that around the internet in the last decade of the 1990s was not a problem. Everyone was on 56Kb/s modems, so approximately 26Kb/s was a manageable chunk of bandwidth.

Dealing with rather more data

Unfortunately for our game, that approach was infeasible. A car does one thing, which is move. The soldiers in the game carry out a variety of actions. Walking, trot-ting, running, sprinting, throwing a spear, waving a sword around, and pushing a siege engine toward city walls are just a small set of the things they could do, each with its own unique animation. This meant that there were six components rather than four components per soldier since we needed to include their action and how far through the action they were. This amounted to 18 bytes per soldier.

Worse, there were rather more than 20 soldiers. In fact, to make things meaning-ful, we needed to accommodate 1,000 soldiers. This made for unattractive arithmetic:

10 Hertz *
1,000 soldiers *
18 bytes *
8 bits =
1,440,000 bits per second

In the early years of this century this was a great big nope. We were just seeing ADSL networks rolling out to households, and upstream bandwidth exceeding 1Mb/s was very rare. The only solution was to send to each client the list of com-mands that had been applied to the soldiers in that update, and to ensure each client applied that list of commands. Since commands were only issued to a unit of a few soldiers every few seconds, this was a much easier load to bear. Of course, this relied on each client maintaining an identical copy of the world, known as synchronous state.

This was a remarkably tricky problem. To remain deterministic, everything had to be identically initialized. If a new data member was added to a struct, it had to be deterministically initialized. Random numbers had to be deterministic. Nothing could be overlooked. Weird things would crop up, like floating-point modes being changed by graphics drivers which would change calculation results on different machines. The biggest problem, though, was preventing the view of the world from interfering with the model of the world.

Each client would have a different window onto the world. The renderer would look at the world and call the const member functions of each thing to get the infor-mation it needed to do its work. Calling the const functions meant that the renderer would not interfere with the world. Unfortunately, const only works so far. Look at this class:

class unit {
public:
  animation* current_animation() const;

private:
  animation* m_animation;
};

The renderer may hold a unit const* object, call the current_animation() function, and make changes to the animation object if one is returned. The unit may not neces-sarily have its own animation: sometimes all the soldiers in a particular unit will share an animation, sometimes they will have their own animation, for example when they are hit by a spear while marching in formation. The const-qualified mem-ber function returns a constant pointer object by value, not a constant animation object by pointer.

The const firewall

There are several solutions to this, such as having pairs of functions where the const function returns an animation const* and the non-const function returns an anima-tion*, but the point was that the abuse of const was subtle and catastrophic. One small change would bubble out to the rest of the world and not be noticed until it was far too late to work backward. It was the butterfly effect writ large.

The const interface, or const firewall as we called it, was of vital importance. It highlighted, in code, in the language itself, which functions were part of the view and which functions were part of the controller. Abuse of the const firewall represented a great disservice to the remainder of the team. The hours required to work out why the world was desynchronizing mounted up throughout production.

As you can imagine, the appearance of const_cast anywhere in the code would set off alarm bells. Painful, baroque interactions between objects could be resolved with a const_cast and programmers had to be reminded constantly of the terrible fate that awaited them. Advertising something as const-safe and then modifying it deceives your clients in the worst possible way.

For example, imagine this function:

float distance_from_primary_marker(soldier const& s);

Anything should be able to safely call this function for every soldier without interfer-ing with the model of the world. It is advertised as a function that makes no change to the soldier at all. Imagine what would happen if, halfway through, this happened:

float distance_from_primary_marker(soldier const& s) {
  …
  const_cast<soldier&>(s).snap_to_meter_grid(); // Oh, the humanity…
  …
}

The calculation performance is improved by working at meter scale, but rather than cache the current value and do all the required arithmetic, the author has simply moved the soldier by a few centimeters, possibly with the intent of restoring its posi-tion later.

What a calamitous approach to take.

Implementing a dual interface

The codebase ended up being scattered throughout with a dual interface, one of which was const and one of which was non-const. Member functions would be duplicated with const qualification, like this:

class soldier {
public:
  commander& get_commander();
  commander const& get_commander() const;
};

Every soldier has a commander, but finding out who it is requires traversing quite a few objects and making a few queries. Rather than duplicating the code and the accompanying maintenance burden, it is tempting to forward to the const-qualified overload, like this:

commander& soldier::get_commander() {
  return const_cast<commander&>(
    static_cast<soldier const&>(*this).get_commander());
}

Although you are const_casting the return type, it is reasonably safe to assume that, since this is a non-const function, this is not a dangerous thing to do. It goes against the spirit of the guideline, though. Fortunately, since the introduction of trailing return types in C++11, there is a better solution:

class soldier {
public:
  commander& get_commander();
  commander const& get_commander() const;

private:
  template <class T>
  static auto get_commander_impl(T& t)
    -> decltype(t.get_commander) {
      // do the work
  }
};

The public get_commander functions simply forward to the static function template. The superior feature of this solution is that the function template knows if it is oper-ating on a soldier const object. The const qualifier is part of the type T. If the implementation breaks const, the compiler will emit an error telling you so. No cast-ing is required, and that is a good thing, since casting is ugly.

This is not always going to work, though. Consider the current_animation example:

class unit {
public:
  animation* current_animation();
  animation const* current_animation() const;

private:
  animation* m_animation;
};

You might look at this and think that the function simply returns a pointer to the animation. Sadly, it is not quite as simple as that, and there is a fair amount of logic to do with animation blending that we need to go through.

It is tempting to carry out the same trick. However, if the calling code expects to modify the animation, and the const-qualified member function does not expect the returned value, which it does not own, to be modified, we have a bug. In this case, though, it is safe to assume that the implementation of each function will be different.

Caching and lazy evaluation

Another example that might tempt you to reach for the const_cast keyword is the caching of expensive computations. There are plenty of these in the domain of mod-eling the world, even a 2km-by-2km subset of it. As we remarked, establishing the identity of the commander is a nontrivial task, so let’s consider trying to preserve the value for later speedy recovery.

class soldier {
public:
  commander& get_commander();
  commander const& get_commander() const;

private:
  commander* m_commander;

  template <class T>
  static auto get_commander_impl(T& t)
    -> decltype(t.get_commander);
};

The rules are such that if the commander has died, a new commander is established. The get_commander_impl function is called many times, while the commander is only likely to die a few times in an entire game, so the trade-off of the extra pointer in exchange for many identical calculations is a good one.

The first thing the function will do is see if the current commander is still alive. If so, the function can make a quick exit, returning that cached value. If not, the function trudges through the objects representing the world, establishing who the commander is and writing the value back to the m_commander member, before deref-erencing the pointer and returning a reference to the entity in question. This is a remarkably costly procedure. We use the word “trudges” advisedly: with so many objects making up the world, and with so many different relationships to maintain and monitor, it occasionally feels like walking through an outdoor festival, yelling out the name of your friend in the hope that they will respond quickly. It is an ideal candidate for caching.

Unfortunately, this function template needs to work with objects of type soldier const as well as of type soldier. Changing the pointer will not be allowed in the for-mer situation. The only solution would seem to be to const_cast t:

template <class T>
auto soldier::get_commander_impl(T& t) -> decltype(t.get_commander) {
  if (!t.m_commander->is_alive()) {
    … // Find the new commander
    const_cast<soldier&>(t).m_commander = new_commander;
  }
  return *t.m_commander;
}

This is not pretty.

Two types of const

There are two ways of apprehending “constness”. The benefit that const delivers is that, upon calling a const-qualified member function, there is no observable differ-ence in the nature of the object. If the object is entirely self-contained and does not reference other objects, in the same way that a pure function references no data external to its scope, then successive calls to const-qualified member functions will always yield the same results.

This does not mean that the representation of the object will remain unchanged. That would be considering the wrong level of abstraction. The implementation is not your concern: the visible interface is what you should be caring about.

In fact, this touches on an important aspect of class design. What does your class own, what does it share ownership of, and what is it merely interested in? In the example of the unit class, part of the member data was a pointer to another object, owned by something else entirely. What does a const qualification mean to the client in that case?

All it can mean is that the function does not change the object it is bound to in any observable way. This is known as logical const. The other type of const is known as bitwise const. This is what is imposed on the function author by const-qualifying a member function. This means that no part of the representation of the object may be changed during execution of the function; this is enforced by the compiler.

The presence of const_cast inside a const-qualified member function should at the very least make you feel a little queasy. You are lying to your clients. Unfortu-nately, the caching example for lazy evaluation is not the only place where you might feel the need to reach for const_cast. What if you want to guarantee that your class is thread safe?

One way of achieving this is to include a mutex in your member data and lock it when your functions are executed. We cannot let that last sentence past your appre-hension without remarking that you should minimize the reach of a mutex: as little data as possible should be covered by it, and it should be part of the smallest possible abstraction. This chimes with Core Guidelines ES.5: “Keep scopes small” and CP.43: “Minimize time spent in a critical section.” Having said all that, using a member mutex brings with it one problem: how do you lock it in a const-qualified member function? That requires changing the mutex.

Now that we have established the difference between logical const and bitwise const, we can introduce the keyword mutable. The specific purpose of this keyword is to respect the logical const nature of an object while not respecting the bitwise const nature. It is not a modern keyword: you will find it in quite old codebases. This keyword decorates a member datum, signifying that it is amenable to modification during execution of a const-qualified member function.

Returning to the unit example, here is how it might be used:

class soldier {
public:
  commander& get_commander();
  commander const& get_commander() const;

private:
  mutable commander* m_commander; // Immune from const constraint

  template <class T>
  static auto get_commander_impl(T& t)
    -> decltype(t.get_commander) {
    if (!t.m_commander->is_alive()) {
      … // Find the new commander
      t.m_commander = new_commander; // Always amenable to change
    }
    return *t.m_commander;
  }
};

The m_commander member is now mutable. This means it is amenable to modification from a const reference.

Surprises with const

Obviously, mutable should not be scattered liberally through your class. It should decorate the data that is used for bookkeeping, rather than for modeling the abstrac-tion. In the case of decorating a mutex member, we can see that this is a sensible policy. In the above example, though, it is not so clear. Raw pointers are confusing things that muddy the waters of ownership, as discussed in Core Guideline I.11: “Never transfer ownership by a raw pointer (T*) or reference (T&),” and they also trip up API design when it comes to separating the view from the controller.

For example, this piece of code is legal:

class int_holder {
public:
  void increment() const { ++ *m_i; }

private:
  std::unique_ptr<int> m_i;
};

This surprises typical users, because they expect to be unable to change the std::unique_ptr object and what it con-tains. In fact, const extends only as far as the std::unique_ptr object. Dereferenc-ing the object is a const operation since it makes no change to the std::unique_ptr. This highlights the importance of recog-nizing the difference between a const pointer and a pointer to const. A pointer to const can be modified but the object it points to cannot. A const pointer cannot be modified but the object it points to can. This is somewhat similar to how references behave: they cannot be reseated, which means they cannot refer to a different object through their lifetime, but the object they refer to can be modified.

mutable should not be scattered liberally through your class. It should decorate the data that is used for bookkeeping, rather than for modeling the abstraction.

Speaking of which, the same surprise is not available for references:

class int_holder {
public:
  void increment() const { ++ m_i; }

private:
  int& m_i;
};

This will not compile. However, it is rarely a good idea to store objects by reference in classes: it entirely disables default assignment since a reference cannot be reseated.

What is needed is a form of the “pImpl” idiom, bundling up a pointer to an object inside a single class and propagating constness. Such an entity has been proposed for standardization and is, at time of writing, available in library fundamentals v2 under the name std::experimental::propagate_const. There are other solutions pending as well: watch the skies.

Summary

In summary:

  • The const keyword is supremely valuable to your interface design and to your clients, cleanly dividing the API between view and control.

  • Do not lie about const by casting it away via const_cast.

  • Understand the difference between logical const and bitwise const.

  • Make use of mutable when appropriate, usually in bookkeeping member data rather than modeling member data.

  • Be aware of how far const will propagate, and be mindful of the difference between a const pointer and a pointer to const.