Compile Time Constants Part 1: Why We Need Them

Compile time constants are an important part of C++. They contribute to program correctness and allow the optimizer to do a better job. Today I will deal with what is possible in terms of compile time constants and compile time calculations in C++03.

The need for compile time constants

There are some things the compiler has to know at compile time. Among those things are types, since C++ is a statically typed language, but also numbers. The most important cases where the compiler needs those numbers at compile time are arrays, case labels and templates.

Arrays

If we want to create an array that is not dynamically allocated, we have to give it a size. That size has to be a number that is known at compile time:

int myIntArray[22];

We have to do this, because the compiler needs to calculate how many memory that variable will occupy. If it’s a stack variable that memory will be reserved in the function’s stack frame. If it’s a member of a class, the compiler needs to know how big an object of that class will be, so it has to know the sizes of its members.

There is an extension that is part of C99, but not of the C++ standard yet. This extension allows for stack based arrays to have a variable length, e.g. like this:

void foo(unsigned length) {
  int variableLengthArray[length];
}

Something like this still won’t work for arrays that are class members. Since it’s not standard C++, compilers like GCC and Clang will compile it but emit warnings, if you configured them right. I’ve rarely seen a case where those variable length arrays were useful.

Back to arrays of compile time constant length. There are cases, where we do not need to specify an array size at all. When we initialize the array with a list of values or a string literal, the compiler will go ahead, count those values and set the size of the array accordingly.

int intArray = {1, 1, 2, 3, 5};
char characterArray[] = "some string";

Note that `characterArray` has length 11, since there will also a trailing `\0` delimiter be stored in the array.

Templates

Besides types, templates can also have integral values as template parameters. Integral values include integral numbers like int, long, short etc., but also bool, characters and enumerators.

enum Color { RED, GREEN, BLUE };

template<unsigned long N, char ID, Color C>
struct Answer {};

Answer<42ul, 'd', BLUE> theAnswer;

Since the compiler instantiates templates at compile time, it is clear that template parameters must be compile time constants. No exception and extension this time, it’s just not possible.

Case labels

The case labels of a switch statement have to be integral compile time constant values, just like non type template parameters.

void comment(int phrase) {
  switch (phrase) {
  case 42: 
    std::cout << "the answer!\n"; 
    break;
  case BLUE: 
    std::cout << "daba di daba da\n"; 
    break;
  case 'y': 
    std::cout << "because!\n"; 
    break;
  default: 
    std::cout << "Whatever...\n";
  }
}

The benefits of compile time constants

There is more to compile time constants than just the technical needs of our compiler. They actually help us and the compiler to produce better programs.

Safety

Compile time constants can help us to better reason about our code. Imagine for example matrix multiplication:

class Matrix {
  unsigned rowCount();
  unsigned columnCount();
  // ...
};

Matrix operator*(Matrix const& lhs, Matrix const& rhs) { /* ... */ }

Two matrices can only be multiplied if the left matrix has as many columns as the right matrix has rows. If that’s not the case, we a re screwed.

  if (lhs.columnCount() != rhs.rowCount()) {
    throw HoustonWeHaveAProblem();
  }

But if we do know the sizes of our matrices at compile time, we can bake those sizes into template parameters. Suddenly matrices of different sizes actually have different types. That way we can write our multiplication in a way that simply does only allow the right kind of matrices to be multiplied:

template <unsigned Rows, unsigned Columns> 
class Matrix {
  /* ... */
};

template <unsigned N, unsigned M, unsigned P>
Matrix<N, P> operator*(Matrix<N, M> const& lhs, Matrix<M, P> const& rhs) { 
  /* ... */
}

Matrix <1, 2> m12 = /* ... */ ;
Matrix <2, 3> m23 = /* ... */;
auto m13 = m12 * m23; //OK, result has type Matrix<1, 3>
auto mX = m23 * m13; //oops. switched the operands -> ERROR!

In this case, the compiler itself prevents the error. There are many more examples, and more complicated ones, that make use of constants in templates. Since C++11 there is a whole bunch of such templates in the standard library. The whole `<type_traits>` library is based on `std::integral_constant`, and `std::array` maps its integral constant template parameter to the size of an array.

Optimizations

Compile time constants allow different kinds of optimizations. For example, if we have a condition that is a compile time constant, the compiler knows always which path will be taken and optimize the other paths away:

if (sizeof(void*) == 4) {
  std::cout << "32 bit\n";
} else {
  std::cout << "64 bit\n";
}

In this case, the compiler can replace the whole if block by one of the two `cout` statements. The rest of the code simply won’t be part of the compiled program.

Another optimization are space optimizations. In general, if we can hold information about our objects as compile time constants, we don’t need to store it in member variables of the objects. We already had examples of that in this post:

  • The class template `Answer` I used as an example at the beginning of this post stores an `unsigned long`, a `Color` and a single character, but the size of its objects is at most one byte.
  • Remember the matrix class template above. We coded the sizes of those matrices into the types themselves. No need to store those values in the individual objects any more.

Conclusion (for now)

As you can see, compile time constants are not only useful, they are an absolute necessity. Mastering their use and distinguishing what can and what can not be done at compile time is extremely important.

In the next weeks I’ll write about how to let the compiler calculate values at compile time. I will especially give an introduction about the so called generalized constant expressions, which have been introduced in C++11/14 to throw the doors to the world of compile time calculations and meta programming wide open.

Previous Post
Next Post
Posted in

5 Comments





  1. Minor correction – characterArray will have length of 12 (some – 4, space – 1, string – 6, – 1, in total 12).

    Reply

  2. Didn’t you mean?

    int intArray[] = {1, 1, 2, 3, 5};

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *