Generic Programming with Templates
Passing a type to a template gives the template the exact type, not an approximation. This preserved detail is the reason the compiler can produce code tailored to that type.
Where Ordinary Functions Run Out of Room
Reusable code is a constant goal in programming. A function that adds two numbers is useful, but a programmer often wants the same logic to work for many kinds of data without rewriting it each time. Templates are a feature designed to address this need. They let a single piece of code serve many situations, and they do so by moving certain decisions out of the running program and into the process that turns source code into an executable.
What do you think happens?
When the compiler meets a template, does it check the template body for type errors right away, or only once the template is used with concrete arguments?
Reveal answer
Only when the template is used with concrete arguments
The source states that type checking in a template is delayed until instantiation, which is the point where the template is used with concrete arguments and a version of it is actually produced. Until then, the template is a pattern that has not yet been filled in.
Three Capabilities Under One Idea
A useful way to understand templates is to ask what they make possible that ordinary functions and ordinary values do not. The answer is that templates give a program three distinct capabilities. Each one addresses a different limit a programmer would otherwise run into. The first is that templates can accept types, values, and other templates as arguments. The second is that type checking in a template is delayed until the template is instantiated. The third is that constant values passed as arguments allow computation to happen at compile time.
All three capabilities share a common theme: each one moves decisions out of the running program and into the compilation process. The diagram below shows how the three ideas relate to one another and to the broader goal of generic programming.
The source describes the three ideas as reinforcing one another. Types carry exact information, checking waits for concrete use, and constants can be evaluated early. Combined, these produce a mechanism that can be very compact and efficient, which is why the source calls it a powerful one.
Passing a Type Without Losing Its Identity
The first capability is that a template can accept a type as an argument, in the same way a function accepts a value. The important phrase is without loss of information. When a type is handed to a template, the template receives the type itself, not a stand-in or a simplified description of it. Because of this, the code that is produced can be built around that exact type.
The same passing mechanism works for values and for templates. The source emphasizes that the ability extends to types as well as values and templates. Keeping all of this information intact is what gives the compiler the chance to optimize, and that opportunity is the subject of the next idea.
Passing a type to a template gives the template the exact type, not an approximation. This preserved detail is the reason the compiler can produce code tailored to that type.
Why Exact Types Create Room for Inlining
Because the exact type survives the trip into the template, the compiler can make its generated code specific to that type. Inlining is the practice of placing the body of a called routine at the point where it is called. The source connects the preserved type information directly to this practice, noting that it creates excellent opportunities for inlining and that current implementations take great advantage of them.
The reasoning can be followed step by step. A template receives a type argument. The type is kept whole. The compiler therefore has a version of the code that is built for that specific type. A routine that exists in a version written for exactly that type is a natural candidate for having its body placed directly at the call site. The source does not go into further detail about the particular optimizations involved, so the key idea to hold on to is simply that exact type information is what makes the specialized code possible.
Delayed Type Checking and Instantiation
The second capability concerns when a template's types are checked. The source states that type checking in templates is delayed until instantiation. Instantiation is the point at which a template is used with concrete arguments and a version of it is actually produced. Until that point, the template is a pattern that has not yet been filled in, so the checking that depends on specific types waits for those specific types to arrive.
The source describes the benefit as an opportunity to weave together information from different contexts. The template itself does not need to know every situation in which it will be used. Each use supplies its own context, and the checking that happens at that moment draws on the information that context provides. A single template can therefore serve many callers, each bringing different information with them.
Compile-Time Constants and factorial<5>
The third capability is the ability to pass constant values as arguments. The source draws the conclusion that this implies the ability to do computation at compile time. The reasoning is direct. If the value handed to a template is a constant, the compiler already knows it when the template is instantiated. Any work that depends only on that constant can therefore be carried out before the program runs.
A small illustrative example makes this concrete. Suppose a template named factorial takes a constant n and is defined so that factorial of n equals n multiplied by factorial of n minus 1, with factorial of 0 equal to 1. The example below is generated for teaching purposes and is not taken from the source. It shows how a constant argument drives a chain of steps that the compiler can finish before execution.
Finishing factorial<5> Before the Program Runs
Using the illustrative definition in which factorial of n equals n multiplied by factorial of n minus 1, with factorial of 0 equal to 1, determine the value of factorial<5> and explain when it is produced.
- 1Start with the constant
The argument 5 is a constant, so the compiler knows it at instantiation. Nothing about it depends on the running program.
- 2Unfold the first step
Using the definition, $5! = 5 \times 4!$. The inner value 4! is still unknown, but it depends only on the constant 4, which is also known.
- 3Keep unfolding
Repeating the definition gives $4! = 4 \times 3!$, then $3! = 3 \times 2!$, then $2! = 2 \times 1!$, then $1! = 1 \times 0!$.
- 4Reach the base case
The definition gives $0! = 1$, which is a fixed value that needs no further unfolding.
- 5Multiply back up
Substituting upward gives $1! = 1$, $2! = 2$, $3! = 6$, $4! = 24$, and finally $5! = 120$.
factorial<5> = 120, a finished value produced during compilation. Because the work depended only on the constant argument, none of it has to happen while the program runs.
Mistakes Learners Make with Template Timing
Common mistake Assuming a template body is fully checked as soon as it is written
What it looks likeA template is written and no error is reported, so the learner concludes every possible type has been verified.
Why it failsThe source says type checking is delayed until instantiation, so the checks that depend on specific types have not yet run when the template is only written.
Do this insteadTreat the template as a pattern. Its type checks happen for each concrete use, so a problem may appear only when a particular set of arguments is supplied.
Common mistake Thinking a type argument is reduced to a simpler description
What it looks likeA learner imagines the template receives a rough label for a type rather than the type itself.
Why it failsThe source stresses that the template receives the type itself, without loss of information, which is why the generated code can be built around that exact type.
Do this insteadThink of a type argument as the complete type, and remember that this completeness is what enables specialized code and inlining.
Common mistake Believing that constant arguments are always evaluated while the program runs
Why it failsWhen a template receives a constant, the compiler already knows that value at instantiation, so work depending only on it can be done before the program runs.
Do this insteadAsk whether the inputs are known at compile time. If they are constants, the related computation can be carried out during compilation.
Common mistake Confusing the three capabilities as separate features rather than one mechanism that reinforces itself
Tracing a Template Through Your Own Uses
Imagine one template used in two places: once with a type that holds whole numbers, and once with a type that holds decimal numbers. Trace what happens for each use. Which steps are shared, which are specific to each use, and at which point would you expect a type problem to be reported if one exists?
Hints
- Start with the template as a pattern, then ask what extra information each call site adds.
- Recall that checking waits for instantiation, so each use triggers its own checks.
- Think about whether the exact type is kept and whether it lets the compiler build code for that type.
Explain in your own words why a constant argument such as 5 allows work to be finished before the program runs, and why the same reasoning would not apply to a value that is only known when the program is running.
Hints
- Consider when the compiler first learns the value.
- Consider what the compiler can finish early if its input is already fixed.
Key Takeaways
- Templates provide three capabilities: passing types, values, and templates as arguments; delaying type checking until instantiation; and passing constant values for compile-time computation.
- Passing a type as an argument keeps its exact information, which lets the compiler build code specific to that type and creates excellent opportunities for inlining.
- Type checking in a template happens at instantiation, so the same template can draw on information from different contexts for each use.
- Constant values passed as arguments let work that depends only on them be completed at compile time, as in the illustrative factorial example.