Numeric Limits and Their Properties
The first is the information numeric_limits supplies, which is a fact about a type.
A Value Known Before the Program Runs
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. This article follows one such call, numeric_limits<int>::max(), from the moment it appears in source code to the moment its value is available as a compile-time constant.
The idea is easier to follow if you keep one question in mind as you read: at what point in the life of a program is this value known? For most values the answer is 'while the program runs.' For the value discussed here, the answer can be 'while the compiler runs,' and that difference is what this article is about.
What Makes a Function Constexpr
A constexpr function is a function that can be evaluated at compile time.
The important word in that definition is 'can.' 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 function can still be called at runtime with values that are only known while the program executes. Because of this, it helps to think of compile-time evaluation as a capability that a function offers, rather than as a separate kind of function with its own rules.
Marking a function constexpr gives it the ability to be used where a value must be known during compilation. It does not force every call to happen at compile time.
Tracing numeric_limits<int>::max() Through the Compiler
The source identifies numeric_limits<int>::max() as an example of a constexpr function. Calling it returns the largest value an int can represent. Because the call is a constexpr function, its result can be used where the language requires a value that is already known during compilation. The diagram below traces the path from the written call to a constant that the rest of the program can use.
Read the diagram from top to bottom. The call starts as ordinary text in the source file. The compiler reaches it while processing the source. Because the function is constexpr, the language allows its result to be computed at that stage. The result, the largest value an int can hold, then becomes a constant that the program can use wherever a known value is needed. The source does not describe every internal detail of this process, so the diagram shows the stages that matter for understanding the outcome: a call is recognized, its result is determined during compilation, and that result is used as a constant.
Following one value from call to use
Trace what happens to the expression std::numeric_limits<int>::max() when it appears in a place that requires a compile-time value.
- 1Step 1: Identify the call
The expression names a constexpr function that returns the largest value an int can represent.
- 2Step 2: Check the context
The surrounding code requires a value that is known during compilation, such as a
static_assertcondition. - 3Step 3: Evaluate the function
Because the function is constexpr and needs no runtime input, its result can be determined while the compiler processes the source.
- 4Step 4: Use the constant
The determined value participates in the compile-time check, so the program does not wait until it runs to decide whether the check passes.
The call produces the largest int value as a compile-time constant that can be used in compile-time checks.
One Function, Two Kinds of Call
A constexpr function does not have a fixed place where it must run. The same 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 runtime 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.
The flowchart separates the two situations. When a call's inputs are known during compilation, the call can be evaluated at that stage. When the inputs are only known while the program executes, the same function is called at runtime. Nothing in the function itself has been rewritten between these cases. Only the timing of the inputs has changed.
Two Ideas Behind numeric_limits
It is useful to separate two ideas. The first is the information numeric_limits supplies, which is a fact about a type. For int, the relevant fact is the largest value it can hold. The second is the mechanism that lets that fact be used during compilation, which is the constexpr nature of the call. Knowing the limit of a type is valuable in itself. The constexpr property is what allows that limit to take part in compile-time checks.
The source pack discusses numeric_limits in terms of limits of numeric types and gives numeric_limits<int>::max() as its constexpr example. It does not walk through the other members of the interface, so this article does not describe them. The point to take away is the relationship: the information is about the type, and the constexpr qualifier controls whether that information can be used where the language requires a known value.
A Compile-Time Check Using the Largest int
The example below is an original illustration of how a constexpr value can be checked by the compiler. It uses numeric_limits<int>::max() in a static_assert, which requires its condition to be known at compile time. The check passes or fails before the program runs, because the value it tests comes from a constexpr function.
- The include brings in the
numeric_limitsclass template. static_assertchecks its condition during compilation; the condition here is a constexpr call.- constexpr int stores a value that is known at compile time, so it can be initialized from the constexpr call.
What do you think happens?
If the static_assert condition were false, would the program fail to compile or fail while running?
Reveal answer
It fails to compile
A static_assert is evaluated during compilation. A false condition is reported as a compile-time error, so the program never runs. In the example, the condition is true on the usual platforms, so the check passes.
Mistakes When Reasoning About Evaluation Time
Common mistake Assuming every call to a constexpr function runs at compile time
What it looks likeCalling the function with a value read from user input and expecting the compiler to compute the result.
Why it failsA constexpr function can be evaluated at compile time, but it can also be called at runtime. Whether it is evaluated early depends on whether its inputs are known during compilation.
Do this insteadAsk where the inputs come from. Known compile-time inputs allow compile-time evaluation; runtime inputs are handled while the program runs.
Common mistake Assuming a runtime value can be used in a static_assert
What it looks likeWriting static_assert(some_variable > 0) where some_variable is only set during execution.
Why it failsA
static_assertneeds its condition known during compilation. A value known only at runtime cannot satisfy that requirement.Do this insteadUse a constexpr value or a constexpr call with known inputs in the condition.
Common mistake Treating the function body as different in compile-time and runtime use
Why it failsThe source describes the same function serving both roles. What changes is when its inputs are known, not the function itself.
Common mistake Confusing the information with the mechanism
Why it failsThe limit of a type is a fact. The constexpr qualifier is what allows that fact to be used in compile-time checks. Both matter, but they are different ideas.
Practice: Classifying Evaluation Time
For each situation, decide whether the constexpr call numeric_limits<int>::max() can be used there without any runtime input. Situation A: a static_assert condition comparing it to zero. Situation B: a comparison with an int read from a file while the program runs. Explain which situation depends only on compile-time knowledge and which depends on runtime values.
Hints
- Ask whether the expression needs any value that is only known while the program runs.
- A
static_assertcondition must be known during compilation. - The function body is the same in both situations; only the timing of the inputs differs.
Reviewing the Compile-Time Path
- 1A constexpr function is a function that can be evaluated at compile time, and it can also be called at runtime.
- 2
numeric_limits<int>::max()is a constexpr call that returns the largest value an int can represent, so its result can be used where a compile-time value is required. - 3Whether a constexpr call is evaluated during compilation depends on whether its inputs are known at that stage.
- 4The information
numeric_limitsprovides is a fact about a type; the constexpr qualifier is what lets that fact take part in compile-time checks.
Key Takeaways
- A constexpr function is a function that can be evaluated at compile time, and the same function can also be called at runtime.
numeric_limits<int>::max()is a constexpr call returning the largest value an int can hold, usable where a compile-time value is needed.- Compile-time evaluation depends on whether the inputs are known during compilation, not on the function body itself.
numeric_limitssupplies facts about types; the constexpr property lets those facts participate in compile-time checks such asstatic_assert.