Concepts / Constant Expressions and Compile-Time Evaluation

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.

  • Programming

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.

cpp
Notes on the code
  • 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

Definition

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.

Aspectconstconstexpr
Question it answersWill the program change this value?Can this value be evaluated at compile time?
Kind of promiseThe value will not be changed after it existsThe 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 roleSpecifying interfaces, such as function parametersSpecifying constants that can be placed in read-only memory
When the value is determinedPossibly at run timeAt 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?

cpp
Notes on the code
  • 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?

  • Yes, it compiles

  • No, it is a compile error

Reveal answer
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?

  • Yes, it compiles

  • No, it is a compile error

Reveal answer
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.

Worked example

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.

  1. 1
    Line 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.

  2. 2
    Line 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.

  3. 3
    Line 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.

Result

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.

constexprinitializerEvery dependencyknown at compiletime?Constant expression,compilesCompile errorexamineyesno
Given a constexpr initializer, does every value it depends on have a compile-time value? A yes answer leads to acceptance, and a no answer leads to a compile error.

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

Definition

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()written in source codeconstexpr functioncallallowed in compile-timecontextsLargest int valueknown during compilationConstant contextsuch as a static_assertcondition or constexprinitializeris a constexpr functionreturnsused where a value must be known
The call is written in source code, it is a constexpr function, and its result becomes a value known during compilation that other constant contexts can use.
cpp
Notes on the code
  • numeric_limits<int>::max() is a constexpr function, so its value can initialize a constexpr variable.
  • static_assert requires 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 like

    Writing constexpr double max3 = 1.4 * square(var); and expecting it to compile because the variable is marked as constant.

    Why it fails

    const 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 instead

    Ask 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 like

    Believing that every call to a constexpr function is computed by the compiler, even when its arguments come from user input.

    Why it fails

    A 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 instead

    Check 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 like

    Believing that const double s1 = sum(v); is an error because sum(v) is not known until the program runs.

    Why it fails

    const 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 instead

    Use 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 like

    Thinking max1 and max2 must behave the same because both write 1.4 * square applied to an int.

    Why it fails

    The 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 instead

    Trace each dependency of the initializer back to its source before deciding whether it is a constant expression.

Trying the Check Yourself

MEDIUM

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.

Related concepts

More concepts starting with C