Introduction to Templates in D
Templates are D's mechanism for compile-time generic programming: a template is parameterized by one or more compile-time arguments (types, values, aliases, or even other templates) and the compiler generates a distinct instantiation for each unique combination of arguments actually used in the program. Unlike C++ templates, D templates have a cleaner, more uniform syntax shared across functions, structs, classes, and interfaces, and are commonly combined with static if, template constraints, and traits from std.traits to produce highly specialized, zero-overhead generic code.
Cricket analogy: A stadium's modular seating design that can be reconfigured for T20, ODI, or Test match capacity using the same underlying blueprint is like a template -- one generic design instantiated differently per format.
Function Templates
A function template is declared with a template parameter list in parentheses before the regular parameter list: T max(T)(T a, T b) { return a > b ? a : b; }. Callers can specify the type explicitly (max!int(3, 5)) or let D's Implicit Function Template Instantiation (IFTI) deduce it from the argument types (max(3, 5)). Each distinct instantiation is compiled separately and independently type-checked, so calling max with int arguments and again with string arguments produces two completely separate functions in the compiled binary, each optimized for its specific type.
Cricket analogy: A generic 'best batting average' comparator that works whether you feed it two ODI averages or two Test averages, instantiated separately for each format, mirrors how max!T generates a distinct compiled function per type.
import std.stdio;
T max(T)(T a, T b)
{
return a > b ? a : b;
}
void main()
{
writeln(max!int(3, 7)); // explicit instantiation: 7
writeln(max(2.5, 1.1)); // IFTI deduces T = double: 2.5
writeln(max("apple", "banana")); // T = string, lexicographic compare
}
Template Constraints and Specialization
Template constraints restrict which types a template accepts, using an if (...) clause after the parameter list that must evaluate to true at compile time, typically built from traits in std.traits like isNumeric!T or isSomeString!T. This lets you write multiple overloads of the same template name that each handle a different category of type, with the compiler selecting the correctly constrained overload -- or producing a clear compile error if no constraint is satisfied -- rather than allowing a template to compile with a type it silently mishandles.
Cricket analogy: A tournament entry rule that only accepts teams with at least three players holding a valid national-team certification is like a template constraint -- only types satisfying isNumeric!T or similar traits are accepted into the instantiation.
std.traits provides dozens of ready-made compile-time predicates for constraints -- isNumeric!T, isSomeString!T, isArray!T, isCallable!T, hasMember!(T, "name") -- and combining several with && or || in an if (...) constraint clause lets you write precise, self-documenting template signatures like auto sum(T)(T[] arr) if (isNumeric!T).
struct and class Templates
Structs and classes can be templated the same way functions are: struct Stack(T) { T[] items; void push(T item) { items ~= item; } T pop() { auto v = items[$-1]; items = items[0 .. $-1]; return v; } }, and you instantiate it with Stack!int or Stack!string. Unlike function templates, struct and class templates are almost never implicitly instantiated from usage alone -- you generally must specify the type argument explicitly (Stack!int stack;), though D does support implicit instantiation of class templates when constructing via a matching constructor in some contexts.
Cricket analogy: A standardized batting-order slot template that can be filled with an opener's profile or a middle-order batsman's profile, generating a distinct concrete lineup entry each time, mirrors instantiating Stack!int versus Stack!string.
import std.stdio;
struct Stack(T)
{
private T[] items;
void push(T item)
{
items ~= item;
}
T pop()
{
auto v = items[$ - 1];
items = items[0 .. $ - 1];
return v;
}
bool empty() const @property
{
return items.length == 0;
}
}
void main()
{
Stack!int intStack;
intStack.push(10);
intStack.push(20);
writeln(intStack.pop()); // 20
Stack!string nameStack;
nameStack.push("Alice");
nameStack.push("Bob");
writeln(nameStack.pop()); // Bob
}
Variadic Templates and static foreach
D supports variadic templates via a parameter pack, e.g. void printAll(Args...)(Args args), where Args captures any number of type arguments and args captures the corresponding values, letting you write functions like a type-safe printf replacement that accepts any mix of types. Inside such templates, static foreach (or the older recursive-template pattern) lets you iterate over the parameter pack at compile time, generating repeated code -- one unrolled iteration per pack element -- rather than a runtime loop, since the pack's length and each element's type must be known during compilation.
Cricket analogy: A team's all-format kit list that must be checked and packed differently for however many formats a tour includes -- Tests, ODIs, T20s -- with each item type handled specifically at the planning stage, mirrors static foreach unrolling a variadic template pack at compile time.
Because template instantiations are generated per unique combination of type arguments, overusing heavily templated generic code across many different types can lead to significant code bloat and longer compile times -- this is the same trade-off C++ template-heavy codebases face. Constrain templates tightly with if (...) clauses and prefer runtime polymorphism (interfaces/virtual functions) when the number of distinct types is large and compile-time specialization isn't providing a meaningful performance benefit.
- Templates parameterize functions, structs, classes, and interfaces over compile-time arguments like types or values.
- IFTI lets the compiler deduce a function template's type arguments from the call site without explicit !T syntax.
- Each unique instantiation is compiled and type-checked separately, producing specialized, zero-overhead code per type.
- Template constraints (if (...) clauses using std.traits predicates) restrict which types can instantiate a template.
- Struct and class templates like Stack!T usually require explicit type arguments at the point of use.
- Variadic templates (Args...) capture an arbitrary number of type arguments, commonly paired with static foreach.
- static foreach unrolls a loop over a compile-time-known pack, generating repeated code rather than looping at runtime.
Practice what you learned
1. What does IFTI stand for and what does it do in D?
2. How are struct and class templates in D typically instantiated, compared to function templates?
3. What is the purpose of a template constraint like if (isNumeric!T)?
4. What does static foreach do inside a variadic template?
5. What is a downside of overusing heavily templated generic code across many types?
Was this page helpful?
You May Also Like
Functions in D
Understand D function declarations, parameter storage classes (in/ref/out/lazy), overloading, and Design by Contract.
Conditionals in D
Learn how D Programming handles branching logic with if-else, switch, the ternary operator, and compile-time static if.
Delegates and Closures
Understand D's delegate type, how closures capture context, and how delegates power callbacks and higher-order functions.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics