C++ improvement “constexpr”

As I discover new improvements to C++, it encourages me to look for more of them. A couple of weeks ago, I was adding a new screen for space combat, and I found myself cutting & pasting code from the existing territory screen. Although I was eager to work on the new space combat screen, I ‘did the right long-term thing’ and started a small project to refactor the common code from several screens into a new shared base class that I could also use for the space combat screen. There were some constants at the top of each .cpp file that are sometimes used in various unique ways by each screen.

const f32 RATIO = 0.86602540378f; // sqrt(3) / 2
const f32 CELL_HEIGHT = 1800.f;
const f32 CELL_WIDTH = CELL_HEIGHT / RATIO;
const f32 FACTOR_X = CELL_WIDTH * 0.75f;
const f32 FACTOR_Y = CELL_HEIGHT;
const f32 SPACER = 0.98f;
const f32 CELL_HEIGHT_SPACER = CELL_HEIGHT * SPACER;
const f32 CELL_WIDTH_SPACER = CELL_WIDTH * SPACER;

These constants needed to be moved to the header file, and there are a bunch of different ways I could move them. I could just declare them directly in the header before the class.

// IDEA 1
const f32 RATIO = 0.86602540378f; // sqrt(3) / 2

class Screen_Base_Hex : public Screen_Base
{
public:

This works, but names like RATIO and CELL_HEIGHT are pretty generic names and likely to be used elsewhere. It would be nice to scope such conveniently short names to the module. I could use a namespace.

// IDEA 2
namespace Screen_Base_Hex_NS
{
const f32 RATIO = 0.86602540378f; // sqrt(3) / 2

class Screen_Base_Hex : public Screen_Base
{
public:
...
} 
} // namespace Screen_Base_Hex_NS

This is really awkward. Either I have to put both the constants and Screen_Base_Hex inside a namespace, which makes Screen_Base_Hex (a fairly unique name) an unwieldly longer name, or I could put RATIO in a namespace that I now have to explicitly specify inside Screen_Base_Hex. Another approach would be to put the constants inside the class Screen_Base_Hex.

// IDEA 3
class Screen_Base_Hex : public Screen_Base
{
public:
  const f32 RATIO = 0.86602540378f; // sqrt(3) / 2

The problem with this is that they are now constant member variables, and they theoretically take up space and need to be initialized with every allocated class instance. I am also referencing the constants in some static functions, and as member variables they are not accessible. I could declare them “static const”.

// IDEA 4
class Screen_Base_Hex : public Screen_Base
{
public:
  static const f32 RATIO;
const f32 Screen_Base_Hex::RATIO = 0.86602540378f; // sqrt(3) / 2

This does make them accessible to static functions, but also requires that I declare the actual value of the constant in the .cpp file, which is a hassle to maintain. Oddly, the standard special cases ints and enums so they can be defined in the header, but not floats! Another alternative is to use a #define macro.

// IDEA 5
#define RATIO (0.86602540378f)

This works, but the constants are no longer scoped to the Screen_Base_Hex class, and although typing is probably preserved in this specific case, idea #1 is more readable and the typing is more explicit in the general case.

I searched to see if there was a better way to do this, and my initial results turned up many, many of the above ideas. I almost gave up and went with idea #4 before I spotted something new in the search results! There is a new keyword “constexpr” that was added in C++11/14 that helps with this problem. It’s used like this:

// FINAL SOLUTION
class Screen_Base_Hex : public Screen_Base
{
public:
  static constexpr f32 RATIO = 0.86602540378f; // sqrt(3) / 2
  static constexpr f32 CELL_HEIGHT = 1800.f;
  static constexpr f32 CELL_WIDTH = CELL_HEIGHT / RATIO;
  static constexpr f32 FACTOR_X = CELL_WIDTH * 0.75f;
  static constexpr f32 FACTOR_Y = CELL_HEIGHT;
  static constexpr f32 SPACER = 0.98f;
  static constexpr f32 CELL_HEIGHT_SPACER = CELL_HEIGHT * SPACER;
  static constexpr f32 CELL_WIDTH_SPACER = CELL_WIDTH * SPACER;

Constexpr tells the compiler explicitly that this value needs to be determined at compile time, and therefore must be provided in the header rather than in the .cpp file, which is cleaner than idea #4.