Chapter 9 Templates

Templates are a way of reusing one piece of code, using different types and constants.

Using a template requires two parts: a template definition, and a template instance. This is a one:many relationship; one template definition may be the code behind multiple template instances.

Templates are supported for functions, structs, classes, unions, and interfaces.

Aggregates

Let’s say we wrote a simple linked list for i32s:

struct I32List
{
	data: i32;
	next: I32List*;

	// Imagine a lot of code here.
}

But if we want to add a list that works on f32s, without templates, we’re stuck maybe adding an extra data field, or copy pasting and making an F32List. Neither of these are great. Enter templates!

struct List!(T)
{
	data: T;
	next: List*;

	// Imagine a lot of code here, too.
}

The above is a template definition. We can’t do anything with that on its own. To use it, we use template instances:

struct I32List = mixin List!i32;
struct F32List = mixin List!f32;

There’s a lot to unpack here, but before getting bogged down in details, lets touch on the results of this code. The result is we have two structs defined, that we can use like a normal struct: I32List, and F32List. The T in the template definition is replaced by i32 and f32 respectively. The use of the definition name, List, in the next field is replaced by I32List and F32List respectively. The mixin means that it is instantiated in the scope where the instances appears, not where the definition exists. This is relevant for import lookups – a non mixin template would need any needed imports at the definition site, a mixin template at the instantiation. Currently, only mixin templates are implemented.

The !(T) portion of the template definition is a list of types and values that the instances have to give. Here’s a more complicated one:

struct DataEntry(Key, Value, MaxSize: i32)
{
	k: Key;
	v: Value;

	fn getMax() i32
	{
		return MaxSize;
	}
}

struct OurDataEntry = mixin DataEntry!(i32, string, 32);

As you can see, when passing arguments to a template instance, if there is only one parameter (like in our List example) we can omit the parens, otherwise parens and commas are used to separate arguments.

If a parameter is just a word (like T, Key, Value) then it is to be replaced by a type given at the template instance. If it is of the form word: type (like MaxSize: i32) then it is a constant value to be given at the template instance.

All the parameters for a given instance are available from outside of the struct, too:

max := OurDataEntry.Max;
v: OurDataEntry.Key;

Using templates with classes, unions, or interfaces is just like we did there, except use class, union, or interface respectively instead of struct, for both the instantiation and definition.

Functions

The other type of template are standalone functions. There’s nothing really different here in how you use them.

fn add!(T)(a: T, b: T) T
{
	return cast(T)(a + b);
}

fn addInteger = add!i32;
fn addFloat   = add!f32;

Static Ifs and Is Expressions

These aren’t technically templates, but templates are where they’re most useful. static if are like regular if statements, but they are evaluated at compile time. This means that the condition must be known at compile time, and evaluate to a boolean.

fn main() i32
{
	static if (1 + 1 == 2) {
		return 3;
	} else {
		return 2;  // This code is not compiled at all.
	}
}

is expressions allow you to query information about a type, at compile time.

static if (is(string == immutable(char)[])) {   // Evaluates to `true`.
	...
}

This allows you to change the way functions in templates behave, depending on the type. The above is the simple equality comparison of the is expression – ‘is the type on the left, the type on the right?’.

Trait Words

These are special words that can go on the right hand side of is expressions, after an @ symbol.

is(T == @isBitsType)   // Is T i8, i16, i32, i64, u8, u16, u32, u64, f32, or f64?
is(T == @isArray)      // Is T an array?
is(T == @isConst)      // Is T const?
is(T == @isImmutable)  // Is T immutable?
is(T == @isScope)      // Is T scope?

Trait Modifiers

Like trait words, but they go on the left hand side of is expressions, to specify a property of the type to check.

/* Is the element of T const? If T is not an array, or the element isn't
 * const, this would be true.
 * e.g. (if T is char, false. If T is char[], false. If T is const(char)[], true.
 * If T is const(char[]), true.
 */
is(@elementOf!T == @isConst)
// Is the key of T an i32? False if T is not an AA.
is(@keyOf!T == i32)
// Is the value of T an i32? False if T is not an AA.
is(@valueOf!T == i32)
// Is the base of T an f32? False if T is not a pointer.
is(@baseOf!T == f32)

There’s no != in an is expression – use !is(...) instead.

Wrapping Up

Next, some parting remarks and some useful tools.


PREV INDEX NEXT