Concepts / Templates and Type Parameters

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.

  • Programming

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?

  • While the program is running

  • While the program is being compiled

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

Definition

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.

Key point

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.

cpp
Walk through it
  1. 1
    The class template SmallBox is only a pattern until the caller names a type.
  2. 2
    When SmallBox<int> appears, the compiler generates a SmallBox version in which T means int.
  3. 3
    The static_assert in that generated version evaluates sizeof(int) and checks the condition.
  4. 4
    When SmallBox<Wide> appears, the compiler generates a second version in which T means Wide.
  5. 5
    The static_assert in the Wide version finds that the condition is false and reports its message.
Notes on the code
  • The template is written once, with T as a placeholder type.
  • The static_assert inside 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.
SmallBox<T>single templateSmallBox<int>assertion passesSmallBox<Wide>assertion failsT = intT = Wide
When the caller uses SmallBox with int and with Wide, which separate versions does the compiler create from the single template?
Worked example

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.

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

  2. 2
    Create the Wide version

    The compiler generates SmallBox for Wide. Each Wide value occupies 24 bytes, so the condition sizeof(T) <= 8 is false.

  3. 3
    Report the failure

    The compiler reports the message attached to the assertion and does not produce a program.

  4. 4
    Check what ran

    Nothing ran, and no value was ever tested at runtime. The whole decision was made while the file was being compiled.

Result

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.

Code walkthrough
static_assert(sizeof(T) <= 8, "T must be 8 bytes or smaller");
  1. 1
    The keyword tells the compiler that this is an assertion to be checked during compilation.
  2. 2
    The condition is first turned into a value for the specific type the caller chose.
  3. 3
    If that value makes the condition true, compilation continues past this line.
  4. 4
    If the condition is false, the compiler reports the attached message and stops compilation.
  • static_assert introduces 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.

Compiler readsstatic_assertCondition true?Compilation continuesno message shownMessage reportedno program producedevaluateyesno
What happens step by step when the compiler evaluates a static_assert condition, and what happens when that condition is false?

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.

Compilationstatic assertion checkedProgram executionruntime check tests valuesthen
Which kinds of errors are caught during compilation, and which only appear while the program is running?
FeatureStatic assertionRuntime check
When it is evaluatedDuring compilationDuring execution
What it testsTypes and compile-time evaluable expressionsValues as the program actually runs
Typical use in generic codeStating requirements on type parametersTesting values that depend on program input
Outcome when falseCompiler reports the message and does not produce a programThe 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 like

    Writing a condition that depends on a variable whose value is read from user input.

    Why it fails

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

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

    Assuming SmallBox<Wide> will compile and only fail when the program runs.

    Why it fails

    The static assertion is evaluated during compilation. A false result stops compilation, so no program is produced.

    Do this instead

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

    The author of generic code cannot see the caller's types in advance, so assumptions that are not stated are not enforced.

    Do this instead

    State the requirement explicitly with a static assertion, and attach a message that explains it.

Practice Predicting the Check

EASY

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.
MEDIUM

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_assert keyword with a condition that refers to sizeof(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.

Related concepts

More concepts starting with T