Report a bug
If you spot a problem with this page, click here to create a Bugzilla issue.
Improve this page
Quickly fork, edit online, and submit a pull request for this page. Requires a signed-in GitHub account. This works well for small changes. If you'd like to make larger changes you may want to consider using a local clone.

Compile-time Sequences

Background

Compile-time sequences are an important metaprogramming concept that comes naturally from D support for variadic templates. They allow a programmer to operate on types, symbols and values, enabling the ability to define compile-time algorithms that operate on types, symbols and values.

Note: For historical reasons these sequences can sometimes be called tuples in documentation or compiler internals, but don't get confused - they don't have much in common with tuples that commonly exist in other languages. Sequences of values of different types that can be returned from functions are provided by std.typecons.Tuple. Using the term "tuple" to mean compile-time sequences is discouraged to avoid confusion, and if encountered should result in a documentation bug report.

Consider this simple variadic template:

template Variadic(T...) { /* ... */ }

T here is a TemplateParameterSequence, which is a core language feature. It has its own special semantics, and, from the programmer's point of view, is most similar to an array of compile-time entities - types, symbols (names) and values. One can check the length of this sequence and access any individual element:

template Variadic(T...)
{
    static assert(T.length > 1);
    pragma(msg, T[0]);
    pragma(msg, T[1]);
}

alias dummy = Variadic!(int, 42);

Output during compilation:

int
42

AliasSeq

The language itself does not provide any means to define such sequences outside of a template parameter declaration. Instead, there is a simple template provided by the standard library:

alias AliasSeq(T...) = T;

All it does is give a specific variadic argument sequence an externally accessible name so that it can be worked with in any other context:

import std.meta;
// can alias to some other name
alias Name = AliasSeq!(int, 42);
pragma(msg, Name[0]);   // int
pragma(msg, Name[1]);   // 42
// or work with a sequence directly
pragma(msg, AliasSeq!("aaa", 0, double)[2]);    // double

Available operations

Checking the length

import std.meta;
static assert(AliasSeq!(1, 2, 3, 4).length == 4);

Indexing and slicing

Indexes must be known at compile-time

import std.meta;

alias nums = AliasSeq!(1, 2, 3, 4);
static assert(nums[1] == 2);

// slice last 3 elements
alias tail = nums[1 .. $];
static assert(tail[0] == 2);
static assert(tail.length == 3);

Element assignment

Works only if the sequence element is a symbol that refers to a mutable variable

import std.meta;

void main()
{
    int x;
    alias seq = AliasSeq!(10, x);
    seq[1] = 42;
    assert(x == 42);
    // seq[0] = 42; // won't compile, can't assign to a constant
}

foreach

D's foreach statement has special semantics when iterating over compile-time sequences. It repeats the body of the loop for each of the sequence elements, with the loop iteration symbol being an alias for each compile-time sequence element in turn.

import std.meta;

void main()
{
    foreach (sym; AliasSeq!(int, "literal", main))
    {
        static if (is(sym))
            pragma (msg, sym);
        else
            pragma (msg, typeof(sym));
    }
}

Output during compilation:

int
string
void()

Note: Static foreach should be preferred in new code.

Auto-expansion

One less obvious property of compile-time argument sequences is that when used as an argument to a function or template, they are automatically treated as a sequence of comma-separated arguments:

import std.meta;

template Print0(T...)
{
    pragma(msg, T[0]);
}

alias Dummy = Print0!(AliasSeq!(int, double));

This will only print int during compilation because the last line gets rewritten as alias Dummy = Print0!(int, double). If auto-expansion didn't happen, AliasSeq!(int, double) would be printed instead. This is an inherent part of the language semantics for variadic sequences, and thus also preserved by AliasSeq.

Homogenous sequences

An AliasSeq that consists of only type or value elements are commonly called "type sequences" or "value sequences" respectively. The concept of a "symbol sequence" is used less frequently but fits the same pattern.

Type sequences

It is possible to use homogenous type sequences in declarations:

import std.meta;
alias Params = AliasSeq!(int, double, string);
void foo(Params); // void foo(int, double, string);

foo(7, 6.5, "hi");

Type sequence instantiation

D supports a special variable declaration syntax where a type sequence acts as a type:

import std.meta;

void foo()
{
    AliasSeq!(int, double, string) variables;
    variables[0] = 42;
    variables[1] = 42.0;
    variables[2] = "just a normal variable";
}

The compiler will rewrite such a declaration to something like this:

int __var1;
double __var2;
string __var3;
alias variables = AliasSeq!(__var1, __var2, __var3);

So a type sequence instance is a kind of symbol sequence that only aliases variables, known as an lvalue sequence.

Variadic template functions use a type sequence instance to declare function parameters:

void foo(T...)(T args)
{
    // 'T' is a type sequence of argument types.
    // 'args' is an instance of T whose elements are the function parameters
    static assert(is(typeof(args) == T));
    args[0] = T[0].init;
}

Value sequences

It is possible to use a sequence of values of the same type to declare an array literal:

import std.meta;
enum e = 3;
enum arr = [ AliasSeq!(1, 2, e) ];
static assert(arr == [ 1, 2, 3 ]);

The above sequence is an rvalue sequence, as it is comprised only of literal values and manifest constants. Each element's value is known at compile-time.

The following demonstrates use of an lvalue sequence:

import std.meta, std.algorithm;
auto x = 3, y = 7;
alias pair = AliasSeq!(x, y);
swap(pair);
assert(x == 7 && y == 3);

As above, such sequences may use element assignment.

.tupleof is a class/struct instance property that provides an lvalue sequence of each field.

A function cannot return a value sequence. Instead, std.typecons.Tuple can be used. It wraps an lvalue sequence in a struct, preventing auto-expansion.

Symbol sequences

A symbol sequence aliases any named symbol - types, variables, functions and templates - but not literals. Like an alias sequence, the kind of elements can be mixed.

import std.meta : AliasSeq;

void f(T)(T v)
{
    pragma(msg, T);
}
alias syms = AliasSeq!(f, f!byte);
syms[0](42); // call f!int with IFTI
syms[1](42); // call f!byte

Output during compilation:

byte
int

Note that function f!byte is instantiated when the sequence is created, not at the call-site.