Compile-Time Evaluation
Both are fixed at compile time, which is exactly why the compiler can check them before any execution happens.
One Piece of Code, Many Possible Types
Suppose you write one function or one class that works for many different data types. The same code can then be used with integers, with floating-point numbers, with characters, or with your own custom types. This flexibility is powerful, but it raises a question: what happens when someone uses your generic code with a type it was never designed for?
This article answers that question with two related tools. The first is the static assertion, a requirement you write down that the compiler checks while it is still translating the source code. The second is the constexpr function, a function the language allows to be evaluated during compilation. Both tools depend on one idea that you should keep in mind throughout: at which moment does the check or the computation happen? The answer determines what kinds of mistakes a check can catch and why these tools are used the way they are.
Type Parameters and the Assumptions They Hide
Generic programming is a programming paradigm that allows you to write code that can work with different data types. Its central mechanism is the type parameter. Instead of writing a function that accepts only one specific type, you write it with a placeholder type, and the actual type is supplied when the code is used. One piece of source code can therefore serve many types.
The placeholder approach has a consequence. When the author writes generic code, the author does not know which concrete types callers will eventually choose. Some of those types may have properties the code needs, and others may not. The author therefore has a reason to state assumptions about the type parameter explicitly, instead of leaving them implicit and hoping every caller obeys them.
A static assertion is the tool for stating those assumptions. The source describes it as an assertion checked by the compiler about types or about compile-time evaluable expressions, and a false result stops compilation. The source identifies the most important use of static assertions as stating and enforcing requirements on type parameters in generic code. Two kinds of subject matter fit inside that statement. The first is a type, such as a question about whether a type has a certain property. The second is any compile-time evaluable expression, meaning an expression whose value the compiler can work out without running the program. Both are fixed at compile time, which is exactly why the compiler can check them before any execution happens.
Predicting When a Size Requirement Gets Checked
What do you think happens?
A generic container contains a static assertion requiring its type parameter to be at most 8 bytes. You write one instance using int and one using a 24-byte struct called Wide. Before reading on, predict which of these produces a program that can run, and when the rejection would happen if one fails.
Reveal answer
The int version compiles, and the Wide version stops compilation with the assertion message.
The size requirement is a compile-time expression, so the compiler evaluates it for each concrete type it is asked to build. The int case satisfies the condition. The Wide case does not, so the compiler reports the attached message and never produces a program. No value is ever tested at runtime.
The following example shows the mechanism in action. It defines a small generic container template. Inside the class body, a static assertion states a requirement on the type parameter T: the size of T must not exceed 8 bytes. The requirement is a compile-time expression, because sizeof is evaluated by the compiler.
static_assertsits inside the class body, so it is part of the template's definitionsizeof(T)is a compile-time expression that the compiler can evaluate for any concrete T- The message string is shown to the programmer when the condition is false
- SmallBox<Wide> triggers the assertion because each Wide occupies 24 bytes
Tracing the Compiler Through the Container Example
Now trace what happens as the compiler reads this file. When the code refers to SmallBox<int>, the compiler creates a version of the template for int. It evaluates sizeof(int), finds a value that satisfies the condition, and the assertion passes, so that line compiles. When the code refers to SmallBox<Wide>, the compiler creates a version of the template for Wide. Each Wide value occupies 24 bytes, so the condition is false. The compiler reports the message attached to the assertion and does not produce a program.
Notice what did not happen. Nothing ran, and no value was ever tested at runtime. The check lived entirely inside the compilation step. The timeline below places that step among the build phases so you can see exactly where the rejection occurs.
Compile-Time Checks Versus Runtime Checks
A runtime check, such as an ordinary assertion that runs while the program executes, tests values as the program actually runs. A static assertion does not wait for execution. The source notes that static assertions are evaluated during compilation while runtime checks are evaluated during execution, so each catches different kinds of problems. The comparison below focuses on the points that matter for generic code.
| Aspect | Static assertion | Runtime check |
|---|---|---|
| When it is evaluated | During compilation | During execution |
| What it tests | Types and compile-time evaluable expressions | Values as the program actually runs |
| What a failure does | Stops compilation and no program is produced | Reports a failure while the program is running |
| Typical use in generic code | Requirements on type parameters | Conditions on data that is only known at run time |
Two checks that catch different kinds of problems
What a constexpr Function Is
A constexpr function is a function that can be evaluated at compile time. The important word 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.
Most 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.
It helps to think of compile-time evaluation as a capability that a function offers, rather than as a separate kind of function. The function body is the same whether the call happens during compilation or at runtime. What changes is when the inputs are known, and therefore when the call can be completed.
Tracing numeric_limits<int>::max() to a Compile-Time Value
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 constexpr, the result can be used where the language requires a value that is already known during compilation. The pipeline below follows that path from the written call to the value a static assertion tests.
The static_assert passes during compilation, so the program builds. No output is printed.- std::
numeric_limits<int>::max()is the same call as the one named in the source; the std:: qualifier names the standard library version - The condition is known during compilation, so the
static_assertcan check it before any execution - The same call inside
main()is evaluated at runtime here, because main runs when the program executes
The static_assert line is the compile-time half of the example. The compiler sees the call, recognizes that numeric_limits<int>::max() is a constexpr function, and computes the largest int value while it translates the file. It then tests the condition greater than zero. Because the result is true, compilation continues.- 1
static_assert(...)asks the compiler to check the condition before any execution - 2
numeric_limits<int>::max()is evaluated during compilation because it is a constexpr function with known inputs - 3The comparison with 0 is then decided at compile time, and a true result lets the build continue
- 4The line inside
main()is a separate call; it is only completed when the program runs
It is useful to separate two ideas. The first is the information numeric_limits supplies, which is a fact about a type: the largest value that type can hold. The second is the mechanism that lets that fact take part in compile-time work, which is the constexpr nature of the call. Knowing a type's limit is valuable in itself, but the constexpr property is what allows the limit to participate in compile-time checks like the one above.
Mistakes Beginners Make With Compile-Time Reasoning
Common mistake Assuming a static assertion checks the data a program receives
What it looks likeExpecting static_assert to reject a user who types a value that is too large
Why it failsA static assertion is evaluated during compilation, before any user input exists. It can only test types and compile-time evaluable expressions.
Do this insteadUse a runtime check for values that are only known while the program executes.
Common mistake Believing a constexpr function always runs at compile time
What it looks likeExpecting every call to numeric_limits<int>::max() to be computed by the compiler
Why it failsA constexpr function can be evaluated at compile time, but the same function can also be called at runtime with values known only while the program runs.
Do this insteadAsk whether the inputs are known during compilation. If they are, the call can be evaluated there; if not, the call happens at runtime.
Common mistake Thinking a generic template is checked only when it is used with a specific type
What it looks likeAssuming a bad requirement in a template body is harmless until some caller misuses it
Why it failsThe author of generic code cannot see the caller's types in advance, so the requirement must be stated in the template and enforced for each type the caller chooses.
Do this insteadWrite static assertions inside the template to state the assumptions the code depends on.
Common mistake Treating a rejected build as a crash, when no program was ever produced
Common mistake Forgetting that numeric_limits supplies a fact about a type, not a value computed from user data
Practice Predicting Compile-Time and Runtime Behavior
A generic class Holder<T> contains static_assert(sizeof(T) <= 4, "T too large"). Which of the following instantiations will stop compilation: Holder<char>, Holder<double>, or Holder<long long> (assume long long is 8 bytes on the platform)? Explain which check decided each answer and in which build phase.
Hints
- Ask whether the condition is a compile-time expression or a value seen at run time
- Compare each type's size with the limit of 4 bytes
- Remember that the rejection happens while the compiler builds the instantiation, before any program runs
Write a static_assert that checks that numeric_limits<int>::max() is greater than numeric_limits<short>::max(). Then explain whether a runtime assert could catch the same mistake if the values were read from a file, and why.
Hints
- Use the std::
numeric_limitsform of the call and compare the two results - Think about whether the two limits are known during compilation
Key Takeaways
- Generic programming uses type parameters so one piece of code can work with many types, and the caller chooses the actual type.
- A static assertion is checked by the compiler during compilation; a false result stops compilation and no program is produced.
- Static assertions test types and compile-time evaluable expressions, while runtime checks test values as the program actually runs.
- A constexpr function can be evaluated at compile time, so a call such as
numeric_limits<int>::max()can produce a value the compiler uses while building the program. - The same constexpr function can also be called at runtime; what decides when it is evaluated is whether its inputs are known during compilation.