Templates: one function, many types
Writing a function once and letting the compiler make a version per type, reading the exclamation mark in code you have already used, and what an error inside a template looks like.
One function, many types
T largest(T)(T a, T b) is a function template. The T before the parentheses is the type of the answer, and the (T) after the name is the list of compile-time parameters: placeholders the compiler fills in. Call it with two ints and the compiler writes a version for int; call it with two strings and it writes a version for string.
The body is written once, but nothing is shared at run time. Two calls with two types produce two ordinary functions, which is why a template costs no speed. What it costs is care: the body has to be written in terms an unknown type can satisfy.
The compiler infers T from the arguments, so the call largest(3, 9) does not need to name it. You can name it when inference is ambiguous or when you want a conversion: largest!double(3, 9) builds the double version and converts both arguments, which is a different function from the one the bare call would make.
Why not take anything at all
A language with a single top type would let one function accept any value, and every operation inside it would be unchecked until it ran. A template keeps the checking: if the type has no comparison, the compiler refuses the call at compile time, before a binary exists.
The gain shows up in an error message. A template instantiated with a type that cannot do the work reports the instance, such as template instance largest!string, and points at the line inside the body that failed. Reading the last error first usually names the type that caused it.
The cost of a template is paid at compile time and in the error messages. A body that works for one type may fail for another in a line that looks unrelated, and the compiler prints the whole instantiation chain. Reading from the bottom up — the innermost line first — is the quickest way through it.
You have been using them all along
The exclamation mark in to!string(x), scores.filter!(s => s >= 88) and sort!((a, b) => a.score > b.score) introduces a template argument. to!string is a template that converts to string because string was named in the brackets; sort! takes the comparison rule as its compile-time argument.
That is the whole idea in one sentence: a template is a function or type with a hole, and the ! is where you say what fills the hole. Everything after this chapter reads as code you already know.
Constraints decide which calls exist
A template can carry a condition after its parameter list: T sum(T)(T[] xs) if (isNumeric!T). The condition is checked when the compiler tries an instantiation. If it fails, that version does not exist, and overload resolution moves on or reports that no call fits. The body is never examined for a type the constraint rejected.
This is how the standard library stays readable: sort accepts a range, not any value, and a mistake names the constraint rather than a page of errors from inside the implementation. std.traits supplies the common tests such as isNumeric and isSomeString, and is(T == string) compares a type directly.
static if chooses at compile time
static if tests a condition the compiler can evaluate and compiles only the branch that passes. It is not a run-time if: the rejected branch does not have to type-check for the chosen type. That is what lets one template body hold a string version, a numeric version and a fallback without any of them breaking the others.
The chain is written static if (...) { ... } else static if (...) { ... } else { ... }, and exactly one branch survives. Used well it is a small dispatch table over types; used badly it hides three functions inside one and makes an error point at the wrong line.
A template with two parameters
import std.stdio;
T largest(T)(T a, T b) {
return a > b ? a : b;
}
void main() {
writeln(largest(3, 9), " ", largest("pear", "apple"));
writeln(largest(2.5, 1.5));
}Largest of three kinds
Run it and count how many versions of largest the compiler made. Then change show to put the value in angle brackets, and add a call that passes two doubles.
Both arguments must have the same type, because one T fills both holes. Printing largest(1, "two") is a compile error rather than a surprise at run time, and that is the feature working as intended.
A sum that only numbers may call
Run it, then call `sum` with an array of strings and read the constraint error before removing the call. Finally add a `static if` branch for `bool` and print a different word for true and false.
Three mistakes: calling a one-type template with two different types, such as largest(1, 2.5), which is a compile error because one T cannot be both; writing a body that assumes an operation the type lacks, so the error appears inside the template rather than at the call; and putting a run-time value in a static if, which the compiler refuses.
Try these next
- Call
sum(["a", "b"])and read the constraint error, then remove the call. - Add a
static ifbranch forbooland print a different word for true and false. - Write a two-parameter template
pair(T, U)that returns both values in a string.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What does the (T) after the function name mean?
