In the previous lesson 5.4 -- Constant expressions and compile-time optimization, we defined what a constant expression is, and why constant expressions are desirable.
A reminder
See 5.4 -- Constant expressions and compile-time optimization for a recap of the benefits of constant expressions.
We also covered that C++ has two types of constant variables: compile-time constant variables, and runtime constant variables. Only compile-time constant variables can be used in constant expressions -- runtime constant variables (and non-constant variables) cannot be.
Since compile-time constant variables have no real downside, we typically want to use compile-time constants wherever possible.
The compile-time const
challenge
Also in the prior lesson (5.4 -- Constant expressions and compile-time optimization), we discussed how one way to make a compile-time constant variable is to use the const
keyword. If the const
variable has an integral type and a constant expression initializer, it is a compile-time constant. All other const
variables are treated as runtime constants.
However, this method has two challenges.
First, when using const
, our integral variables could end up as either a compile-time const or a runtime const, depending on whether the initializer is a constant expression or not. In some cases, this can make it hard to tell whether the const variable is actually a compile-time constant or not.
For example:
int a { 5 }; // not const at all
const int b { a }; // obviously a runtime const (since initializer is non-const)
const int c { 5 }; // obviously a compile-time const (since initializer is a constant expression)
const int d { someVar }; // not obvious whether this is a runtime or compile-time const
const int e { getValue() }; // not obvious whether this is a runtime or compile-time const
In the above example, both d
and e
could be either a runtime constant or a compile-time constant depending on how someVar
and getValue()
are defined. It’s not clear until we hunt down the definitions for those identifiers (which may require hunting down the definition of the initializers for those variables!)
Second, this use of const
to create compile-time constant variables does not extend to non-integral variables. And there are many cases where we would like non-integral variables to be compile-time constants too.
The constexpr
keyword
Fortunately, we can enlist the compiler’s help to ensure we get a compile-time constant variable where we desire one. To do so, we use the constexpr
keyword (which is shorthand for “constant expression”) instead of const
in a variable’s declaration. A constexpr variable is always a compile-time constant. As a result, a constexpr variable must be initialized with a constant expression, otherwise a compilation error will result.
For example:
#include <iostream>
// The return value of a non-constexpr function is not a constant expression
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;
}
Because functions normally execute at runtime, the return value of a function is not a constant expression (even when the value returned by the return statement is). This is why five()
is not a legal initialization value for constexpr int f
.
Related content
We talk about functions whose return values can be used in constant expressions in lesson 5.8 -- Constexpr and consteval functions.
Additionally, constexpr
works for variables with non-integral types:
constexpr double d { 1.2 }; // d can be used in constant expressions!
The meaning of const vs constexpr for variables
For variables:
const
means that the value of an object cannot be changed after initialization. The value of the initializer may be known at compile-time or runtime. The const object can be evaluated at runtime.constexpr
means that the object can be used in a constant expression. The value of the initializer must be known at compile-time. The constexpr object can be evaluated at runtime or compile-time.
Constexpr variables are implicitly const. Const variables are not implicitly constexpr (except for const integral variables with a constant expression initializer). Although a variable can be defined as both constexpr
and const
, in most cases this is redundant, and we only need to use either const
or constexpr
.
Unlike const
, constexpr
is not part of an object’s type. Therefore an variable defined as constexpr int
actually has type const int
(due to the implicit const
that constexpr
provides for objects).
Best practice
Any constant variable whose initializer is a constant expression should be declared as constexpr
.
Any constant variable whose initializer is not a constant expression (making it a runtime constant) should be declared as const
.
Caveat: In the future we will discuss some types that are not fully compatible with constexpr
(including std::string
, std::vector
, and other types that use dynamic memory allocation). For constant objects of these types, either use const
instead of constexpr
, or pick a different type that is constexpr compatible (e.g. std::string_view
or std::array
).
Nomenclature
The term constexpr
is a portmanteau of “constant expression”. This name was picked because constexpr objects (and functions) can be used in constant expressions.
Formally, the keyword constexpr
applies only to objects and functions. Conventionally, the term constexpr
is used as shorthand for any constant expression (such as 1 + 2
).
Author’s note
Some of the examples on this site were written prior to the best practice to use constexpr
-- as a result, you will note that some examples do not follow the above best practice. We are currently in the process of updating non-compliant examples as we run across them.
Const and constexpr function parameters
Normal function calls are evaluated at runtime, with the supplied arguments being used to initialize the function’s parameters. Because the initialization of function parameters happens at runtime, this leads to two consequences:
const
function parameters are treated as runtime constants (even when the supplied argument is a compile-time constant).- Function parameters cannot be declared as
constexpr
, since their initialization value isn’t determined until runtime.
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 5.8 -- Constexpr and consteval functions.
C++ also supports a way to pass compile-time constants to a function. We discuss these in lesson 11.9 -- Non-type template parameters.
Nomenclature recap
Term | Definition |
---|---|
Compile-time constant | A value or non-modifiable object whose value must be known at compile time (e.g. literals and constexpr variables). |
Constexpr | Keyword that declares variables as compile-time constants (and functions that can be evaluated at compile-time). Informally, shorthand for “constant expression”. |
Constant expression | An expression that contains only compile-time constants and operators/functions that support compile-time evaluation. |
Runtime expression | An expression that is not a constant expression. |
Runtime constant | A value or non-modifiable object that is not a compile-time constant. |