Concepts / Numeric Limits and Their Properties

Numeric Limits and Their Properties

The first is the information numeric_limits supplies, which is a fact about a type.

  • Programming

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

Definition

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.

Key point

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.

Source code callnumeric_limits<int>::max()Compiler finds thecallwhile processing sourceConstexpr functionrecognizedallowed in compile-timecontextResult: largest intvalueknown during compilationConstant used inprogramwhere a compile-time valueis requiredencounteredcheckedevaluatedembedded
What steps does the compiler take, in order, to turn the call numeric_limits<int>::max() in source code into a constant 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.

Worked example

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.

  1. 1
    Step 1: Identify the call

    The expression names a constexpr function that returns the largest value an int can represent.

  2. 2
    Step 2: Check the context

    The surrounding code requires a value that is known during compilation, such as a static_assert condition.

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

  4. 4
    Step 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.

Result

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.

Constexpr functioncallsame function bodyInputs known duringcompilation?Evaluated at compiletimeresult becomes a constantCalled at runtimeinputs known only whilerunningyesno
Which parts of a constexpr function call happen during compilation, and which are deferred to program execution, depending on how the call is used?

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.

cpp
Notes on the code
  • The include brings in the numeric_limits class template.
  • static_assert checks 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?

  • It fails to compile

  • It fails while running

  • It runs normally and ignores the check

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

    Calling the function with a value read from user input and expecting the compiler to compute the result.

    Why it fails

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

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

    Writing static_assert(some_variable > 0) where some_variable is only set during execution.

    Why it fails

    A static_assert needs its condition known during compilation. A value known only at runtime cannot satisfy that requirement.

    Do this instead

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

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

    The 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

EASY

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_assert condition 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

  1. 1
    A constexpr function is a function that can be evaluated at compile time, and it can also be called at runtime.
  2. 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.
  3. 3
    Whether a constexpr call is evaluated during compilation depends on whether its inputs are known at that stage.
  4. 4
    The information numeric_limits provides 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_limits supplies facts about types; the constexpr property lets those facts participate in compile-time checks such as static_assert.

Related concepts

More concepts starting with N