Consider the following short program:
#include <iostream>
int main()
{
int x { 3 + 4 };
std::cout << x << '\n';
return 0;
}
The output is straightforward:
7
However, there’s an interesting optimization possibility hidden within.
If this program were compiled exactly as it was written (with no optimizations), the compiler would generate an executable that calculates the result of 3 + 4
at runtime (when the program is run). If the program were executed a million times, 3 + 4
would be evaluated a million times, and the resulting value of 7
produced a million times. But note that the result of 3 + 4
never changes -- it is always 7
. So re-evaluating 3 + 4
every time the program is run is wasteful.
Constant expressions
A constant expression is an expression that can be evaluated by the compiler at compile-time. To be a constant expression, all the values in the expression must be known at compile-time (and all of the operators and functions called must support compile-time evaluation).
When the compiler encounters a constant expression, it may evaluate the expression at compile-time, and then replace the constant expression with the result of the evaluation.
In the above program, the expression 3 + 4
is a constant expression. So when this program is compiled, the compiler may evaluate constant expression 3 + 4
and then replace the constant expression 3 + 4
with the resulting value 7
. In other words, the compiler may actually compile this:
#include <iostream>
int main()
{
int x { 7 };
std::cout << x << '\n';
return 0;
}
This program produces the same output (7
), but the resulting executable no longer needs to spend CPU cycles calculating 3 + 4
at runtime!
Note that the expression std::cout << x
is not a constant expression, because our program can’t output values to the console at compile-time. So this expression will always evaluate at runtime.
Key insight
Evaluating constant expressions at compile-time makes our compilation take longer (because the compiler has to do more work), but such expressions only need to be evaluated once (rather than every time the program is run). The resulting executables are faster and use less memory.
Compile-time constants
A Compile-time constant is a constant whose value is known at compile-time. Literals (e.g. ‘1’, ‘2.3’, and “Hello, world!”) are one type of compile-time constant.
But what about const variables? Const variables may or may not be compile-time constants.
Compile-time const
A const variable is a compile-time constant if its initializer is a constant expression.
Consider a program similar to the above that uses const variables:
#include <iostream>
int main()
{
const int x { 3 }; // x is a compile-time const
const int y { 4 }; // y is a compile-time const
const int z { x + y }; // x + y is a constant expression, so z is compile-time const
std::cout << z << '\n';
return 0;
}
Because the initialization values of x
and y
are constant expressions, x
and y
are compile-time constants. This means x + y
is also constant expression. So when the compiler compiles this program, it can evaluate x + y
for their values, and replace the constant expression with the resulting literal 7
.
Note that the initializer of a compile-time const can be any constant expression. All of the following will be compile-time const variables:
const int z { 3 }; // 3 is a constant expression, so z is compile-time const
const int a { 1 + 2 }; // 1 + 2 is a constant expression, so a is compile-time const
const int b { z * 2 }; // z * 2 is a constant expression, so b is compile-time const
Compile-time const variables are often used as symbolic constants:
const double gravity { 9.8 };
Compile-time constants enable the compiler to perform optimizations that aren’t available with non-compile-time constants. For example, whenever gravity
is used, the compiler can simply substitute the identifier gravity
with the literal double 9.8
, which avoids having to fetch the value from somewhere in memory.
In many cases, compile-time constants will be optimized out of the program entirely. In cases where this is not possible (or when optimizations are turned off), the variable will still be created (and initialized) at runtime.
Runtime const
Any const variable that is initialized with a non-constant expression is a runtime constant. Runtime constants are constants whose initialization values aren’t known until runtime.
The following example illustrates the use of a constant that is a runtime constant:
#include <iostream>
int getNumber()
{
std::cout << "Enter a number: ";
int y{};
std::cin >> y;
return y;
}
int main()
{
const int x{ 3 }; // x is a compile time constant
const int y{ getNumber() }; // y is a runtime constant
const int z{ x + y }; // x + y is a runtime expression
std::cout << z << '\n'; // this is also a runtime expression
return 0;
}
Even though y
is const, the initialization value (the return value of getNumber()
) isn’t known until runtime. Thus, y
is a runtime constant, not a compile-time constant. And as such, the expression x + y
is a runtime expression.
The constexpr
keyword
When you declare a const variable, the compiler will implicitly keep track of whether it’s a runtime or compile-time constant. In most cases, this doesn’t matter for anything other than optimization purposes, but there are a few cases where C++ requires a constant expression (we’ll cover these cases later as we introduce those topics). And only compile-time constant variables can be used in a constant expression.
Because compile-time constants also allow for better optimization (and have little downside), we typically want to use compile-time constants wherever possible.
When using const
, our variables could end up as either a compile-time const or a runtime const, depending on whether the initializer is a compile-time expression or not. In some cases, it can be hard to tell whether a const variable is a compile-time const (and usable in a constant expression) or a runtime const (and not usable in a constant expression).
For example:
int x { 5 }; // not const at all
const int y { x }; // obviously a runtime const (since initializer is non-const)
const int z { 5 }; // obviously a compile-time const (since initializer is a constant expression)
const int w { getValue() } // not obvious whether this is a runtime or compile-time const
In the above example, w
could be either a runtime or a compile-time const depending on how getValue()
is defined. It’s not at all clear!
Fortunately, we can enlist the compiler’s help to ensure we get a compile-time const where we desire one. To do so, we use the constexpr
keyword instead of const
in a variable’s declaration. A constexpr (which is short for “constant expression”) variable can only be a compile-time constant. If the initialization value of a constexpr variable is not a constant expression, the compiler will error.
For example:
#include <iostream>
int five()
{
return 5;
}
int main()
{
constexpr double gravity { 9.8 }; // ok: 9.8 is a constant expression
constexpr int sum { 4 + 5 }; // ok: 4 + 5 is a constant expression
constexpr int something { sum }; // ok: sum is a constant expression
std::cout << "Enter your age: ";
int age{};
std::cin >> age;
constexpr int myAge { age }; // compile error: age is not a constant expression
constexpr int f { five() }; // compile error: return value of five() is not a constant expression
return 0;
}
Best practice
Any variable that should not be modifiable after initialization and whose initializer is known at compile-time should be declared as constexpr
.
Any variable that should not be modifiable after initialization and whose initializer is not known at compile-time should be declared as const
.
Although function parameters can be const
, they cannot be constexpr
.
Related content
C++ does support functions that can be evaluated at compile-time (and thus can be used in constant expressions) -- we discuss these in lesson 6.14 -- Constexpr and consteval functions.
When are constant expressions actually evaluated?
The compiler is only required to evaluate constant expressions at compile-time in contexts that require a constant expression (such as the initializer of a constexpr variable):
constexpr int x { 3 + 4 }; // 3 + 4 will always evaluate at compile time
In contexts that do not require a constant expression, the compiler may choose whether to evaluate a constant expression at compile-time or at runtime.
int x { 3 + 4 }; // 3 + 4 may evaluate at compile-time or runtime
In the above variable definition, x
is not a constexpr variable and the initialization value does not need to be known at compile-time. Thus the compiler is free to choose whether to evaluate 3 + 4
at compile-time or runtime.
Even though it is not strictly required, modern compilers will usually evaluate a constant expression at compile-time because it is an easy optimization and more performant to do so.
Constant folding for constant subexpressions
Consider the following example:
#include <iostream>
int main()
{
constexpr int x { 3 + 4 }; // 3 + 4 is a constant expression
std::cout << x << '\n'; // this is a runtime expression
return 0;
}
3 + 4
is a constant expression, so the compiler will evaluate 3 + 4
at compile-time, and replace it with value 7
. The compiler will likely optimize x
out of the above program, replacing std::cout << x << '\n'
with std::cout << 7 << '\n'
. The output expression will execute at runtime.
However, because x
is only used once, it’s more likely we’d write the program like this in the first place:
#include <iostream>
int main()
{
std::cout << 3 + 4 << '\n'; // this is a runtime expression
return 0;
}
Since the full expression std::cout << 3 + 4 << '\n'
is not a constant expression, it’s reasonable to wonder whether the constant subexpression 3 + 4
will still be optimized at compile-time. The answer is generally “yes”. Compilers have long been able to optimize constant subexpressions, including variables whose values can be determined at compile-time (compile-time const and constexpr variables).
As an aside…
This optimization process is called “constant folding”.
Making our variables constexpr ensures that those variables have values known at compile-time, and thus are eligible for constant folding when they are used in expressions (even in non-const expressions).