Templates and Type Parameters
A static assertion is an assertion checked by the compiler about types or compile-time evaluable expressions. The most important use of static assertions is stating and enforcing requirements on type parameters in generic code.
One Function, Many 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 introduces the tool that answers that question, the static assertion, and shows how it is tied to templates and type parameters.
What do you think happens?
A generic box template is written once. A caller then creates a box holding a very large struct type. Does the rejection of that type happen while the program is being compiled, or while the program is running?
Reveal answer
While the program is being compiled
A static assertion is evaluated during compilation. A false result stops compilation, so no program is produced and nothing ever runs. Runtime checks, by contrast, are evaluated during execution.
As you read the rest of this article, keep one question in mind: at which moment does the check happen? The answer determines what kinds of mistakes the check can catch, and it is the key to understanding why static assertions are used the way they are.
Why Type Parameters Need Checks
Generic programming writes one piece of code that works with many types by using type parameters, and the actual type is chosen by the caller.
The central mechanism of generic programming is the type parameter. Instead of writing a function that accepts only one specific type, you write it with a placeholder type. The actual type is supplied when the code is used, so one piece of source code can serve many types.
This placeholder approach has a consequence. When the author writes the generic code, the author does not know which concrete types callers will eventually choose. Some of those types may have the 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 an assertion checked by the compiler about types or compile-time evaluable expressions. The most important use of static assertions is stating and enforcing requirements on type parameters in generic code.
The two kinds of subject matter in that definition are fixed at compile time. 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. Because both are known during compilation, the compiler is able to check them before any execution happens.
Tracing One Template Into Several Versions
The following example was written for this article to show 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.
- 1The class template SmallBox is only a pattern until the caller names a type.
- 2When SmallBox<int> appears, the compiler generates a SmallBox version in which T means int.
- 3The
static_assertin that generated version evaluatessizeof(int)and checks the condition. - 4When SmallBox<Wide> appears, the compiler generates a second version in which T means Wide.
- 5The
static_assertin the Wide version finds that the condition is false and reports its message.
- The template is written once, with T as a placeholder type.
- The
static_assertinside the class body states the requirement on T. - SmallBox<int> asks the compiler to create a version of the template for int.
- SmallBox<Wide> asks the compiler to create a version of the template for Wide.
Tracing the Compiler Through Both Uses
The file contains SmallBox<int> and SmallBox<Wide>. Determine which uses compile and what the compiler does with each.
- 1Create the int version
The compiler generates SmallBox for int. It evaluates
sizeof(int), finds a value that satisfies the condition of being 8 or less, and the assertion passes. That line compiles. - 2Create the Wide version
The compiler generates SmallBox for Wide. Each Wide value occupies 24 bytes, so the condition
sizeof(T)<= 8 is false. - 3Report the failure
The compiler reports the message attached to the assertion and does not produce a program.
- 4Check what ran
Nothing ran, and no value was ever tested at runtime. The whole decision was made while the file was being compiled.
SmallBox<int> compiles. SmallBox<Wide> is rejected at compile time, and no program is produced.
The example illustrates the pattern the source identifies as the most important use of static assertions. The author of SmallBox wrote a requirement about T, and the compiler enforced that requirement for every type a caller chose. The author never had to see those callers or their types in advance.
Writing a Static Assertion
A static assertion is written with the static_assert keyword. It takes a compile-time condition and a message. In a template, the condition usually refers to the type parameter, such as sizeof(T), so that the compiler can evaluate it separately for each type the caller supplies. The message is what the compiler reports when the condition is false.
static_assert(sizeof(T) <= 8, "T must be 8 bytes or smaller");- 1The keyword tells the compiler that this is an assertion to be checked during compilation.
- 2The condition is first turned into a value for the specific type the caller chose.
- 3If that value makes the condition true, compilation continues past this line.
- 4If the condition is false, the compiler reports the attached message and stops compilation.
static_assertintroduces the compile-time check.sizeof(T)is an expression the compiler can evaluate once T is known.- <= 8 is the requirement the generic code depends on.
- The string is the message reported when the condition is false.
The flowchart below shows the same evaluation as a sequence of decisions. Notice that the only two outcomes are continuing compilation or reporting a message and producing no program.
Compile-Time 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. Static assertions are evaluated during compilation, while runtime checks are evaluated during execution, so each catches different kinds of problems.
| Feature | 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 |
| Typical use in generic code | Stating requirements on type parameters | Testing values that depend on program input |
| Outcome when false | Compiler reports the message and does not produce a program | The problem is discovered only when the program reaches that check |
Static assertions and runtime checks catch different kinds of problems.
The practical consequence is that a type mistake caught by a static assertion never reaches the running program. A runtime check can only catch a problem after the program has been built and reached the point where the value is tested.
Mistakes With Static Assertions
Common mistake Expecting a static assertion to test a value that exists only at runtime
What it looks likeWriting a condition that depends on a variable whose value is read from user input.
Why it failsA static assertion is checked by the compiler, and it works with types or compile-time evaluable expressions. A runtime value is not known during compilation.
Do this insteadUse a static assertion for requirements about types and constants, and use a runtime check for values that exist only while the program runs.
Common mistake Thinking the check happens when the program starts
What it looks likeAssuming SmallBox<Wide> will compile and only fail when the program runs.
Why it failsThe static assertion is evaluated during compilation. A false result stops compilation, so no program is produced.
Do this insteadAsk at which moment the check happens. For static assertions, the answer is compile time.
Common mistake Leaving the requirement on the type parameter implicit
Why it failsThe author of generic code cannot see the caller's types in advance, so assumptions that are not stated are not enforced.
Do this insteadState the requirement explicitly with a static assertion, and attach a message that explains it.
Practice Predicting the Check
A template contains static_assert(sizeof(T) <= 8, "T must be 8 bytes or smaller"). A caller uses it with the Wide type, whose values occupy 24 bytes. Will the program be produced, and at what moment is the decision made?
Hints
- Compare the condition
sizeof(T)<= 8 with the 24-byte size of Wide. - Recall when a static assertion is evaluated and what happens when its condition is false.
Write a static assertion inside a template Holder<T> that requires sizeof(T) to be 16 or less, and give it a message that explains the requirement.
Hints
- Use the
static_assertkeyword with a condition that refers tosizeof(T). - The message is the second argument and is reported when the condition is false.
Key Takeaways
- Generic programming writes one piece of code that works with many types by using type parameters, and the caller chooses the actual type.
- A static assertion is checked by the compiler about types or compile-time evaluable expressions, and a false result stops compilation.
- Static assertions are most useful for stating and enforcing requirements on type parameters, because the template author cannot see the caller's types in advance.
- Static assertions are evaluated during compilation and catch type-level problems, while runtime checks are evaluated during execution and test values as the program runs.