Constant Expressions and Compile-Time Evaluation
const means roughly 'I promise not to change this value,' and the compiler enforces that promise. It is used primarily to specify interfaces.
Two Keywords, Two Different Questions
Most of the values a program uses are not known until it runs. A user types a number, a file is read, or a calculation depends on earlier results. Some values, however, are fixed by the language and its types themselves. The largest value an int can hold is one example. C++ lets certain functions take part in this kind of early work, so that their results can be used while the compiler is still processing the source code.
C++ offers two keywords for working with values that should not change: const and constexpr. Both are often described as ways to make something constant, which makes them easy to confuse. They answer different questions, though. const is about a promise that the program makes not to change a value. constexpr is about when a value can be computed, specifically whether it can be evaluated at compile time. The difference between these two questions is the key to using either keyword correctly.
What const Promises
const means roughly 'I promise not to change this value,' and the compiler enforces that promise. It is used primarily to specify interfaces. When data is passed to a function, a const qualifier lets the programmer say that the function will not modify it, so the data can be passed without fear of it being changed.
- The const in the parameter type is the promise: sum will not modify its argument.
- Because the promise is part of the declaration, callers can rely on it without reading the function body.
The important detail is what const does not require. A const value does not have to be known at compile time. A line such as const double s1 = sum(v); is acceptable even though sum(v) is computed while the program runs. The const still applies to s1 afterward. This is the central point of this section: const constrains how a value may be used after it exists, not when that value must be computed.
What constexpr Demands
A constant expression is an expression that can be evaluated at compile time. A constexpr variable must be initialized with one. The keyword constexpr means roughly 'to be evaluated at compile time.'
constexpr is used primarily to specify constants. The source gives two reasons for this: it allows data to be placed in read-only memory, where it is unlikely to be corrupted, and it supports performance. A constexpr declaration therefore makes a stronger demand than const does. The initializer must be computable by the compiler before the program runs, and every value the initializer depends on must itself be known at that stage.
A value can be const without being constexpr. This is why const alone is not enough when you need a value fixed at compile time, and why constexpr is not needed for every value you promise not to change. The two keywords are related, but they make different demands.
| Aspect | const | constexpr |
|---|---|---|
| Question it answers | Will the program change this value? | Can this value be evaluated at compile time? |
| Kind of promise | The value will not be changed after it exists | The initializer must be computable before the program runs |
| Can the initializer be a run-time call? | Yes, such as sum(v) | No, an ordinary variable or run-time call is an error |
| Typical role | Specifying interfaces, such as function parameters | Specifying constants that can be placed in read-only memory |
| When the value is determined | Possibly at run time | At compile time |
const and constexpr answer different questions
Predicting Which Initializations Compile
The examples in this section use three kinds of values: a named constant declared with const, an ordinary variable that is not declared constant, and a vector of doubles that is also not a constant. The question for every line is the same: does the compiler have enough information to evaluate this initialization before the program runs?
- dmv is a named constant, so
square(dmv)can be evaluated by the compiler. - var is an ordinary variable, so
square(var)is not a constant expression. - max3 is only const, so it may be computed when the program runs.
sum(v)depends on the ordinary vector v, so it cannot initialize a constexpr variable.
What do you think happens?
Will the line constexpr double max1 = 1.4 * square(dmv); compile, given that dmv is a const int with the value 17?
Reveal answer
It compiles.
dmv is a named constant, so square(dmv) is a constant expression and the compiler can evaluate the whole initializer before the program runs. Only the variable dmv is changed to var in the next line, and that single change is what breaks it.
What do you think happens?
The line constexpr double max2 = 1.4 * square(var); has the same shape as max1, but var is an ordinary int. Does it compile?
Reveal answer
It is a compile error.
var is an ordinary variable, so it is not a constant expression, and the initializer of a constexpr variable must be one.
Checking the three lines in order
For each line in the example, decide whether the initializer is a constant expression, and whether the line compiles.
- 1Line one: constexpr with dmv
The initializer uses square applied to dmv, a named constant with value 17. The compiler can compute the whole value before the program runs, so the initializer is a constant expression and the line compiles.
- 2Line two: constexpr with var
The initializer uses square applied to var, an ordinary int. The compiler cannot know its value before the program runs, so the initializer is not a constant expression and the constexpr requirement fails.
- 3Line three: const with var
The same initializer is now used with only const. const does not demand compile-time evaluation, so the value may be computed at run time and the line is allowed.
Only the constexpr line that depends on an ordinary variable fails. The shape of the expression is identical in lines one and two; what changes is whether the compiler knows the inputs.
Checking a constexpr Initializer Step by Step
When you are unsure whether a constexpr initialization will compile, apply the rule from the source directly. Ask whether every value that the initializer depends on is one the compiler can evaluate at compile time. The flowchart below captures that check for a constexpr initializer.
Applying this check to max1 gives a yes, since dmv is a named constant. Applying it to max2 gives a no, since var is an ordinary variable. The question is about the dependencies of the initializer, not about how the expression looks on the page.
What a constexpr Function Is
A constexpr function is a function that can be evaluated at compile time. The important idea is the phrase 'can be evaluated.' A constexpr function is not simply a function that runs early. It is a function that the language allows to be used in compile-time contexts.
The same constexpr function can serve two roles. In a compile-time context, its arguments must be known during compilation, and its result becomes part of the compiled program's constant information. In a run-time context, the same function can be called with values that only become available while the program executes. The function body is the same in both cases. What changes is when the inputs are known, and therefore when the call can be completed.
How numeric_limits Reaches a Compile-Time Value
The source identifies numeric_limits<int>::max() as a constexpr function. Calling it returns the largest value an int can represent. Because that call is a constexpr function, its result can be used where the language requires a value that is already known during compilation. Follow the path from source text to compile-time value in the diagram below.
numeric_limits<int>::max()is a constexpr function, so its value can initialize a constexpr variable.static_assertrequires its condition to be known at compile time, and this condition comes from a constexpr function.- This is an original illustration of the idea in the source, not a listing from it.
Notice the two roles in the example. The first line uses the call to set a constant value. The static_assert uses the same call inside a condition that must be settled before the program runs. The source describes an assert of this kind as working for the same reason: the tested value comes from a constexpr function.
What numeric_limits Provides
numeric_limits provides information about the limits of numeric types. It is a fact about a type, not a computation the program performs on its data. Knowing the limit of int is valuable in itself, for example when a program needs to check that a value stays within range.
The constexpr nature of the call is a separate idea. It is the mechanism that lets that fact take part in compile-time checks, such as the static_assert above. Keeping these two ideas apart makes it easier to reason about why a particular numeric_limits call can appear in a constant context while another function call cannot.
Mistakes Beginners Make with Compile-Time Evaluation
Common mistake Treating const and constexpr as the same thing
What it looks likeWriting constexpr double max3 = 1.4 * square(var); and expecting it to compile because the variable is marked as constant.
Why it failsconst is a promise not to change the value. constexpr is a demand that the value be computable at compile time. The second requirement fails when the initializer depends on an ordinary variable.
Do this insteadAsk two separate questions: will the value be changed, and can the compiler evaluate it before the program runs? Use const for the first and constexpr for the second.
Common mistake Assuming a constexpr function always runs at compile time
What it looks likeBelieving that every call to a constexpr function is computed by the compiler, even when its arguments come from user input.
Why it failsA constexpr function can be evaluated at compile time, and the same function can also be called at runtime with values known only while the program executes.
Do this insteadCheck whether the inputs are known during compilation. Only then is the call usable in a constant context.
Common mistake Assuming a const value must be known at compile time
What it looks likeBelieving that const double s1 = sum(v); is an error because sum(v) is not known until the program runs.
Why it failsconst does not require the initializer to be known at compile time. The source example explicitly allows a const value computed at run time.
Do this insteadUse const to promise that the value will not change after it exists. Do not expect const to force compile-time evaluation.
Common mistake Judging by the shape of the expression rather than its inputs
What it looks likeThinking max1 and max2 must behave the same because both write 1.4 * square applied to an int.
Why it failsThe outcome depends on whether the input is a named constant or an ordinary variable. Replacing dmv with var changes the result even though the expression has the same shape.
Do this insteadTrace each dependency of the initializer back to its source before deciding whether it is a constant expression.
Trying the Check Yourself
Given const int limit = 100; and int count = 5;, decide whether each of the following compiles: (a) constexpr int a = square(limit); (b) constexpr int b = square(count); (c) const int c = square(count); where square is a constexpr function. Justify each answer by naming the dependency that decides it.
Hints
- For each initializer, list every value it depends on and ask whether the compiler knows that value before the program runs.
- A named const with a known value counts as a known value. An ordinary variable does not.
Key Takeaways
- const is a promise not to change a value, enforced by the compiler, and it is used mainly to specify interfaces such as function parameters.
- constexpr demands that an initializer be a constant expression, meaning the compiler can evaluate it before the program runs.
- A value can be const without being constexpr, so a const value may be computed at run time, such as
sum(v). - A constexpr function can be evaluated at compile time with known inputs and still be called at run time, and
numeric_limits<int>::max()is one example whose result can be used in constant contexts.