3.6. Keyword: const

Use const to instruct the compiler to hold something constant. The const keyword can modify the behavior of anything, depending on where it is used:

Programs typically use a lot of constants. Mathematical constants like \(\pi\), or \(e\), and unit conversions are common. Obviously, it would be bad if a program changed the value of \(\pi\) in the middle of its execution. As programmers, we can make a promise to never change those variables that should not change. But the language allows a way to enforce the idea.

A const is a named constant. It can’t be assigned a new value after initialization. For example:

const double pi = 3.14159265359;

double area = pi * r * r;           // OK to read pi like any other variable

pi = 3;                             // this is a compile error

Returning to our by_reference function from the previous section, we have seen that pass by reference makes passing data to functions cheap:

void by_reference (int& x) {
  std::cout << "in by_ref the address of x is   " << &x << '\n';
  x = -1;
}

But one of the side-effects of making passing cheap, is that now the called function might change the value provided. As programmers, we could promise to not change values in functions that shouldn’t, but generally, we don’t like promises. Programmers prefer contracts. Fortunately, const allows us to define just such a contract in the declaration:

void by_reference (const int& x) {
  std::cout << "in by_ref the address of x is   " << &x << '\n';
  x = -1;  // this is now a compile error
}

This is called passing by constant reference, or “const ref”, for short. Passing by const ref allows us to enforce our intentions. If a variable is declared as const, it is a compile error to change it. Any callers can use this function and rest assured that their data cannot change. The more time you spend programming, the more you will appreciate how powerful that guarantee is.

C99 added the const keyword, so now it’s in both languages, but you don’t see it nearly as often in C. Many programmers use #define instead.

3.6.1. Prefer const to #define

We also prefer inline specifier and enum declaration over define.

There are good reasons to avoid #define where alternatives exist.

#define is parsed by the preprocessor, not the compiler. This mean that effectively, all #define directives are literally strings. Fundamentally they are no different from any other pre-processor directive (#include, #ifdef, etc.), except that people commonly use #define as a placeholder for a numeric type, or a function.

For example:

#define ASPECT_RATIO 1.653

is an old fashioned way to define a constant, but you’ll likely see it ‘in the wild’. The pre-processor literally copies the value ‘1.653’ every place in the source code it encounters the string ‘ASPECT_RATIO’. Then the program is compiled.

Prefer this instead:

const double ASPECT_RATIO = 1.653;

This version preserves the name ‘ASPECT_RATIO’ which can simplify debugging. It is possible to also preserve macro names using certain debugging compiler switches, such as -g3.

3.6.1.1. Function-like macros using #define

If you use #define to create a function-like macro, then unexpected behaviors are possible. For example, a macro to call some function f() with the larger of either a or b:

#include <iostream>

#define CALL_WITH_MAX(a,b) f((a) > (b) ? (a) : (b))

int f(const int x) {
  return x;
}

int main() {
  int a = -5;
  const int b = 0;

  for (int i = 1; i < 11; ++i) {
    CALL_WITH_MAX(++a, b);                  // call f, but throw away the result
    std::cout << i << ", a: " << a << '\n'; // a is one larger each iteration
  }

  a = -5;
  for (int i = 1; i < 11; ++i) {
    CALL_WITH_MAX(++a, b+10);
    std::cout << i << ", a: " << a << '\n';
  }
}

The number of times a is incremented depends on how CALL_WITH_MAX is called. In this case, the value of b affects whether a is incremented once or twice. Ouch!

Step through example 17 and see for yourself.

#define may seem like a shortcut. It’s not.

Use it when no better alternative exists.

3.6.2. Keyword: constexpr

The keyword constexpr was added in C++11.

The fundamental difference between const and constexpr is that constexpr must be able to be determined at compile time. So while this is OK:

constexpr double pi = 3.14159265359;

// and so is this
constexpr double pi = acos(-1);

// and so is this
constexpr double area (const double radius) {
   return pi * radius * radius;
}

This is not OK in C++11:

constexpr double area (const double radius) {
   assert (radius > 0);
   return pi * radius * radius;
}

Adding a simple assertion causes this function to no longer compile:

g++ -std=c++11 -Wall -Wextra -pedantic     area.cpp   -o area
area.cpp: In function ‘constexpr double area(double)’:
area.cpp:8:1: error: body of constexpr function ‘constexpr double area(double)’ not a return-statement
  }
  ^

On compilers that support C++14, if you compile with -std=c++11 you may see a warning like:

warning: use of this statement in a constexpr function is a C++14 extension [-Wc++14-extensions]
  assert (radius > 0);

There are some exceptions, but in C++11, any function more complex than return (some_expression) is not able to be evaluated at compile time, therefore, it won’t compile as a constexpr expression. You should still use it when you can.

The rules for constexpr changed in C++14 and more in C++17. Although not available on the Mesa server, you should still read the docs and be aware of what is and is not a valid constant expression for whatever environment you are working in.

3.6.3. Guidelines for now

  • When creating local variables

    • Ask: “Does this variable ever change?”

    • If not, consider const or constexpr

  • Recall constexpr is more restrictive

    • Constant expression is evaluated at compile time

  • When passing parameters to functions

    • Consider passing by const reference

      • Applies only to object types

        • Pass fundamental types by const value if they should not change

      • Avoids making an extra (unneeded) copy

      • Prevents unintended modification

Try This!

Given the following:

How many simple changes can you make to the function area that are valid if the function signature is

const double area (const double radius)

but invalid if the function signature is unchanged?


More to Explore

You have attempted of activities on this page