템플릿

템플릿 (Templates)

템플릿은 D의 제네릭 프로그래밍(generic programming) 접근 방식이에요. 템플릿은 타입, 값, 심볼, 또는 시퀀스를 매개변수로 받을 수 있어요. 이 문서는 템플릿 선언, 인스턴스화, 각종 매개변수(타입/this/값/alias/시퀀스), 에포니머스(eponymous) 템플릿, 애그리게이트 타입 템플릿, 함수 템플릿, 중첩/재귀 템플릿, 그리고 템플릿 제약(constraints)을 다루어요.

출처: Templates

본문

템플릿은 D의 제네릭 프로그래밍 접근 방식이에요. 템플릿은 TemplateDeclaration으로 정의될 수 있어요:

TemplateDeclaration:
    template Identifier TemplateParameters Constraintopt { DeclDefsopt }

TemplateParameters:
    ( TemplateParameterListopt )

TemplateParameterList:
    TemplateParameter
    TemplateParameter ,
    TemplateParameter , TemplateParameterList

템플릿의 DeclDefs 본문은 절대 인스턴스화되지 않더라도 문법적으로 올바르게(syntactically correct) 유지되어야 해요. 의미적 분석(semantic analysis)은 인스턴스화될 때까지 수행되지 않아요. 템플릿은 자신만의 스코프를 형성하고, 템플릿 본문은 클래스, struct, 타입, enum, 변수, 함수, 그리고 다른 템플릿 같은 선언을 포함할 수 있어요.

템플릿 매개변수는 타입, 값, 심볼, 또는 시퀀스를 받을 수 있어요.

template t(T) // declare type parameter T
{
    T v; // declare a member variable of type T within template t
}

같은 Identifier를 가진 여러 템플릿이 선언되면, 매개변수가 다르거나 다르게 특수화되면(specialized) 구별돼요.

템플릿이 템플릿과 같은 식별자를 가진 멤버를 가지면, 그 템플릿은 에포니머스 템플릿(Eponymous Template)이에요. 에포니머스 멤버가 하나인 template 선언은 보통 대신 특정 짧은 문법의 template 선언으로 작성돼요.

템플릿 인스턴스화 (Template Instantiation)

템플릿은 사용 전에 인스턴스화되어야 해요. 이는 템플릿에 인자 목록을 전달하는 것을 의미해요. 그 인자들은 전형적으로 템플릿 본문에 치환(substitute)되며, 이는 새로운 스코프의 엔티티가 돼요.

함수 템플릿은 컴파일러가 함수 호출에서 템플릿 인자를 추론할 수 있으면 암시적으로 인스턴스화될 수 있어요. 그렇지 않으면 템플릿은 명시적으로 인스턴스화되어야 해요.

명시적 템플릿 인스턴스화 (Explicit Template Instantiation)

템플릿은 템플릿 이름 뒤에 !, 그 다음 인자 목록 또는 단일 토큰 인자를 사용해 명시적으로 인스턴스화돼요.

TemplateInstance:
    Identifier TemplateArguments

TemplateArguments:
    ! ( TemplateArgumentListopt )
    ! TemplateSingleArgument

TemplateArgumentList:
    TemplateArgument
    TemplateArgument ,
    TemplateArgument , TemplateArgumentList

TemplateSingleArgument:
    Identifier
    FundamentalType
    CharacterLiteral
    StringLiteral
    InterpolationExpressionSequence
    IntegerLiteral
    FloatLiteral
    true
    false
    null
    this
    SpecialKeyword
    Vector

템플릿 인자는 타입, 컴파일 타임 표현식, 또는 심볼일 수 있어요.

TemplateArgument:
    Type
    AssignExpression
    Symbol

Symbol:
    SymbolTail
    . SymbolTail

SymbolTail:
    Identifier
    Identifier . SymbolTail
    TemplateInstance
    TemplateInstance . SymbolTail

일단 인스턴스화되면, 템플릿 멤버(template members)라 불리는 템플릿 안의 선언들은 TemplateInstance의 스코프에 있어요:

template TFoo(T) { alias Ptr = T*; }
...
TFoo!(int).Ptr x; // declare x to be of type int*

TemplateArgument가 한 토큰 길이면 괄호를 생략할 수 있어요:

TFoo!int.Ptr x;   // same as TFoo!(int).Ptr x;

템플릿 인스턴스화는 앨리어스될 수 있어요:

template TFoo(T) { alias Ptr = T*; }
alias foo = TFoo!(int);
foo.Ptr x;        // declare x to be of type int*

공통 인스턴스화 (Common Instantiation)

같은 TemplateArgumentList를 가진 TemplateDeclaration의 여러 인스턴스화는 모두 같은 인스턴스를 가리켜요. 예를 들어:

template TFoo(T) { T f; }
alias a = TFoo!(int);
alias b = TFoo!(int);
...
a.f = 3;
assert(b.f == 3);  // a and b refer to the same instance of TFoo

이는 TemplateInstance가 다른 모듈에서 수행되어도 마찬가지예요. 템플릿 인자가 같은 템플릿 매개변수 타입으로 암시적으로 변환되어도 여전히 같은 인스턴스를 가리켜요. 이 예제는 TemplateValueParameter와 struct 템플릿을 사용해요:

struct TFoo(int x) { }

// Different template parameters create different struct types
static assert(!is(TFoo!(3) == TFoo!(2)));
// 3 and 2+1 are both 3 of type int - same TFoo instance
static assert(is(TFoo!(3) == TFoo!(2 + 1)));

// 3u is implicitly converted to 3 to match int parameter,
// and refers to exactly the same instance as TFoo!(3)
static assert(is(TFoo!(3) == TFoo!(3u)));

실용적 예제 (Practical Example)

간단한 제네릭 복사 템플릿은 다음과 같아요:

template TCopy(T)
{
    void copy(out T to, T from)
    {
        to = from;
    }
}

이 템플릿을 사용하려면 먼저 특정 타입으로 인스턴스화해야 해요:

int i;
TCopy!(int).copy(i, 3);

인스턴스화 스코프 (Instantiation Scope)

TemplateInstance는 TemplateDeclaration이 선언된 스코프에서 항상 인스턴스화돼요. 추가로 템플릿 매개변수들이 그들의 추론된 타입에 대한 앨리어스로 선언돼요.

예제 — module a:

template TFoo(T) { void bar() { func(); } }

module b:

import a;

void func() { }
alias f = TFoo!(int); // error: func not defined in module a

예제 — module a:

template TFoo(T) { void bar() { func(1); } }
void func(double d) { }

module b:

import a;

void func(int i) { }
alias f = TFoo!(int);
...
f.bar();  // will call a.func(double)

TemplateParameter 특수화와 기본 인자(default arguments)는 TemplateDeclaration의 스코프에서 평가돼요.

템플릿 매개변수 (Template Parameters)

TemplateParameter:
    TemplateTypeParameter
    TemplateValueParameter
    TemplateAliasParameter
    TemplateSequenceParameter
    TemplateThisParameter

템플릿 매개변수는 타입, 값, 심볼, 또는 시퀀스를 받을 수 있어요.

  • 타입 매개변수(type parameters)는 어떤 타입이든 받을 수 있어요.
  • 값 매개변수(value parameters)는 컴파일 타임에 정적으로 평가될 수 있는 어떤 표현식이든 받을 수 있어요.
  • alias 매개변수는 거의 모든 심볼을 받을 수 있어요.
  • 시퀀스 매개변수(sequence parameters)는 0개 이상의 타입, 값, 심볼을 받을 수 있어요.

템플릿 매개변수는 TemplateParameter가 받아들일 수 있는 인자를 제약하는 특수화(specialization)를 가질 수 있어요.

template t(T : int) // type T must implicitly convert to int
{
    ...
}

기본 인자(default argument)는 일치하는 인자가 제공되지 않을 때 TemplateParameter에 사용할 타입, 값 또는 심볼을 지정해요.

타입 매개변수 (Type Parameters)

TemplateTypeParameter:
    Identifier TemplateTypeParameterSpecializationopt TemplateTypeParameterDefaultopt

TemplateTypeParameterSpecialization:
    : Type

TemplateTypeParameterDefault:
    = Type
특수화와 패턴 매칭 (Specialization and Pattern Matching)

템플릿은 템플릿 매개변수 식별자 뒤에 :와 특수화 타입의 패턴을 붙여 특정 타입의 인자들에 대해 특수화될 수 있어요. 예를 들어:

template TFoo(T)        { ... } // #1
template TFoo(T : T[])  { ... } // #2
template TFoo(T : char) { ... } // #3
template TFoo(T, U, V)  { ... } // #4

alias foo1 = TFoo!(int);            // instantiates #1
alias foo2 = TFoo!(double[]);       // instantiates #2 matching pattern T[] with T being double
alias foo3 = TFoo!(char);           // instantiates #3
alias fooe = TFoo!(char, int);      // error, number of arguments mismatch
alias foo4 = TFoo!(char, int, int); // instantiates #4

인스턴스화하기 위해 선택된 템플릿은 TemplateArgumentList의 타입들에 맞는 것 중 가장 특수화된 것이에요. 결과가 모호하면 오류예요.

타입 매개변수 추론 (Type Parameter Deduction)

템플릿 매개변수의 타입은 특정 템플릿 인스턴스화에 대해 템플릿 인자를 해당 템플릿 매개변수와 비교하여 추론돼요. 각 템플릿 매개변수에 대해, 각 매개변수에 대해 타입이 추론될 때까지 다음 규칙이 순서대로 적용돼요:

  1. 매개변수에 타입 특수화가 없으면, 매개변수의 타입은 템플릿 인자로 설정돼요.
  2. 타입 특수화가 타입 매개변수에 의존하면, 그 매개변수의 타입은 타입 인자의 해당 부분으로 설정돼요.
  3. 모든 타입 인자를 검사한 후 타입이 할당되지 않고 남은 타입 매개변수가 있으면, TemplateArgumentList의 같은 위치에 있는 템플릿 인자에 대응하는 타입이 할당돼요.
  4. 위 규칙을 적용해도 각 템플릿 매개변수에 대해 정확히 하나의 타입이 나오지 않으면 오류예요.

예를 들어:

template TFoo(T) { }
alias foo1 = TFoo!(int);     // (1) T is deduced to be int
alias foo2 = TFoo!(char*);   // (1) T is deduced to be char*

template TBar(T : T*) { }    // match template argument against T* pattern
alias bar = TBar!(char*);    // (2) T is deduced to be char

template TAbc(D, U : D[]) { }    // D[] is pattern to be matched
alias abc1 = TAbc!(int, int[]);  // (2) D is deduced to be int, U is int[]
alias abc2 = TAbc!(char, int[]); // (4) error, D is both char and int

template TDef(D : E*, E) { }   // E* is pattern to be matched
alias def = TDef!(int*, int);  // (1) E is int
                               // (3) D is int*

특수화로부터의 추론은 하나 이상의 매개변수에 값을 제공할 수 있어요:

template Foo(T: T[U], U)
{
    ...
}

Foo!(int[long])  // instantiates Foo with T set to int, U set to long

일치를 고려할 때, 클래스는 어떤 슈퍼 클래스나 인터페이스에 대한 일치로 간주돼요:

class A { }
class B : A { }

template TFoo(T : A) { }
alias foo = TFoo!(B);      // (3) T is B

template TBar(T : U*, U : A) { }
alias bar = TBar!(B*, B);  // (2) T is B*
                           // (3) U is B

This 매개변수 (This Parameters)

TemplateThisParameter:
    this TemplateTypeParameter

TemplateThisParameter는 멤버 함수 템플릿에서 this 참조의 타입을 잡아내는 데 사용돼요. 또한 this 참조의 변경 가능성(mutability)을 추론해요. 예를 들어, this가 const면 함수는 const로 표시돼요.

struct S
{
    void foo(this T)() const
    {
        pragma(msg, T);
    }
}

void main()
{
    const(S) s;
    (&s).foo();
    S s2;
    s2.foo();
    immutable(S) s3;
    s3.foo();
}

출력:

const(S)
S
immutable(S)
런타임 타입 검사 피하기 (Avoiding Runtime Type Checks)

TemplateThisParameter는 상속과 함께 사용할 때 특히 유용해요. 예를 들어, 파생 클래스 타입을 반환하는 final 기본 메서드의 구현을 생각해 봐요. 전형적으로 이것은 기본 타입을 반환하겠지만, 그러면 그 타입의 파생 속성을 호출하거나 접근하는 것이 금지될 거예요:

interface Addable(T)
{
    final auto add(T t)
    {
        return this;
    }
}

class List(T) : Addable!T
{
    List remove(T t)
    {
        return this;
    }
}

void main()
{
    auto list = new List!int;
    list.add(1).remove(1);  // error: no 'remove' method for Addable!int
}

여기서 add 메서드는 remove 메서드를 구현하지 않는 기본 타입을 반환해요. 템플릿 this 매개변수를 이 목적에 사용할 수 있어요:

interface Addable(T)
{
    final R add(this R)(T t)
    {
        return cast(R)this;  // cast is necessary, but safe
    }
}

class List(T) : Addable!T
{
    List remove(T t)
    {
        return this;
    }
}

void main()
{
    auto list = new List!int;
    static assert(is(typeof(list.add(1)) == List!int));
    list.add(1).remove(1);  // ok, List.add

    Addable!int a = list;
    // a.add calls Addable.add
    static assert(is(typeof(a.add(1)) == Addable!int));
}

값 매개변수 (Value Parameters)

TemplateValueParameter:
    BasicType Declarator TemplateValueParameterSpecializationopt TemplateValueParameterDefaultopt

TemplateValueParameterSpecialization:
    : ConditionalExpression

TemplateValueParameterDefault:
    = AssignExpression
    = SpecialKeyword

템플릿 값 매개변수는 컴파일 타임에 정적으로 평가될 수 있는 다음 중 하나의 표현식을 인자로 받을 수 있어요:

  • bool, 문자, 정수, 부동소수점 또는 문자열 값
  • null
  • 요소들이 유효한 템플릿 값 인자가 될 수 있는 배열 리터럴
  • 키와 값이 각각 유효한 템플릿 값 인자가 될 수 있는 연관 배열 리터럴
  • 인자들이 유효한 템플릿 값 인자가 될 수 있는 struct 리터럴
template foo(string s)
{
    enum string bar = s ~ " betty";
}

void main()
{
    import std.stdio;
    writeln(foo!("hello").bar); // prints: hello betty
}
특수화 (Specialization)

제공된 어떤 특수화나 기본 표현식도 컴파일 타임에 평가 가능해야 해요. 이 예제에서 템플릿 foo는 10에 대해 특수화된 값 매개변수를 가져요:

template foo(U : int, int v : 10)
{
    U x = v;
}

void main()
{
    assert(foo!(int, 10).x == 10);
    static assert(!__traits(compiles, foo!(int, 11)));
}

이는 특정 값에 대해 다른 템플릿 본문이 필요할 때 유용할 수 있어요. 다른 정수 리터럴 값을 받도록 다른 템플릿 오버로드가 정의될 거예요.

Alias 매개변수 (Alias Parameters)

TemplateAliasParameter:
    alias Identifier TemplateAliasParameterSpecializationopt TemplateAliasParameterDefaultopt
    alias BasicType Declarator TemplateAliasParameterSpecializationopt TemplateAliasParameterDefaultopt

TemplateAliasParameterSpecialization:
    : Type
    : ConditionalExpression

TemplateAliasParameterDefault:
    = Type
    = ConditionalExpression

Alias 매개변수는 템플릿이 컴파일 타임에 계산된 심볼 이름이나 값으로 매개변수화되게 해줘요. 타입 이름, 전역 이름, 지역 이름, 모듈 이름, 템플릿 이름, 템플릿 인스턴스 등 거의 모든 종류의 D 심볼을 사용할 수 있어요.

심볼 앨리어스 (Symbol Aliases)

타입 이름 (Type names):

class Foo
{
    static int x;
}

template Bar(alias a)
{
    alias sym = a.x;
}

void main()
{
    alias bar = Bar!(Foo);
    bar.sym = 3;  // sets Foo.x to 3
    assert(Foo.x == 3);
}

전역 이름 (Global names):

shared int x;

template Foo(alias var)
{
    auto ptr = &var;
}

void main()
{
    alias bar = Foo!(x);
    *bar.ptr = 3;       // set x to 3
    assert(x == 3);

    static shared int y;
    alias abc = Foo!(y);
    *abc.ptr = 3;       // set y to 3
    assert(y == 3);
}

지역 이름 (Local names):

template Foo(alias var)
{
    void inc() { var++; }
}

void main()
{
    int v = 4;
    alias foo = Foo!v;
    foo.inc();
    assert(v == 5);
}

모듈 이름 (Module names):

import std.conv;

template Foo(alias a)
{
    alias sym = a.text;
}

void main()
{
    alias bar = Foo!(std.conv);
    string s = bar.sym(3);   // calls std.conv.text(3)
    assert(s == "3");
}

템플릿 이름 (Template names):

shared int x;

template Foo(alias var)
{
    auto ptr = &var;
}

template Bar(alias Tem)
{
    alias instance = Tem!(x);
}

void main()
{
    alias bar = Bar!(Foo);
    *bar.instance.ptr = 3;  // sets x to 3
    assert(x == 3);
}

템플릿 인스턴스 이름 (Template instance names):

shared int x;

template Foo(alias var)
{
    auto ptr = &var;
}

template Bar(alias sym)
{
    alias p = sym.ptr;
}

void main()
{
    alias foo = Foo!(x);
    alias bar = Bar!(foo);
    *bar.p = 3;  // sets x to 3
    assert(x == 3);
}
값 앨리어스 (Value Aliases)

리터럴 (Literals):

template Foo(alias x, alias y)
{
    static int i = x;
    static string s = y;
}

void main()
{
    import std.stdio;
    alias foo = Foo!(3, "bar");
    writeln(foo.i, foo.s);  // prints 3bar
}

컴파일 타임 값 (Compile-time values):

template Foo(alias x)
{
    static int i = x;
}

void main()
{
    // compile-time argument evaluation
    enum two = 1 + 1;
    alias foo = Foo!(5 * two);
    assert(foo.i == 10);
    static assert(foo.stringof == "Foo!10");

    // compile-time function evaluation
    int get10() { return 10; }
    alias bar = Foo!(get10());
    // bar is the same template instance as foo
    assert(&bar.i is &foo.i);
}

함수 리터럴 (Function Literals):

template Foo(alias fun)
{
    enum val = fun(2);
}

alias foo = Foo!((int x) => x * x);
static assert(foo.val == 4);
타입화된 Alias 매개변수 (Typed Alias Parameters)

Alias 매개변수는 타입화될 수도 있어요. 이 매개변수들은 그 타입의 심볼을 받아들여요:

template Foo(alias int p) { alias a = p; }

void fun()
{
    int i = 0;
    Foo!i.a++;  // ok
    assert(i == 1);

    float f;
    //Foo!f;  // fails to instantiate
}
특수화 (Specialization)

Alias 매개변수 특수화는 매개변수가 일치할 인자를 제약해요. 특수화는 타입 또는 컴파일 타임 표현식일 수 있어요.

타입 특수화: 인자는 특수화와 같은 타입 심볼을 가리켜야 해요. const 같은 타입 한정자는 무시되므로, const(S)S는 같은 특수화로 취급돼요.

struct S {}
struct T {}

template Foo(alias s : S) {}

alias f1 = Foo!S;         // ok
alias f2 = Foo!(const(S)); // ok: qualifiers ignored
//alias f3 = Foo!T;       // error: T is not S
//alias f4 = Foo!1;       // error: 1 is not a type symbol

표현식 특수화: 인자는 특수화와 같은 값으로 평가되는 컴파일 타임 표현식이어야 해요.

template Bar(alias n : 3) {}

alias b1 = Bar!3;       // ok
alias b2 = Bar!(1 + 2); // ok: evaluates to 3
//alias b3 = Bar!4;     // error: 4 != 3

Alias 매개변수는 리터럴과 사용자 정의 타입 심볼 둘 다 받아들일 수 있지만, 타입 매개변수와 값 매개변수의 일치보다는 덜 특수화돼요:

template Foo(T)         { ... }  // #1
template Foo(int n)     { ... }  // #2
template Foo(alias sym) { ... }  // #3

struct S {}
int var;

alias foo1  = Foo!(S);      // instantiates #1
alias foo2  = Foo!(1);      // instantiates #2
alias foo3a = Foo!([1,2]);  // instantiates #3
alias foo3b = Foo!(var);    // instantiates #3
template Bar(alias A) { ... }                 // #4
template Bar(T : U!V, alias U, V...) { ... }  // #5

class C(T) {}
alias bar = Bar!(C!int);    // instantiates #5

시퀀스 매개변수 (Sequence Parameters)

TemplateSequenceParameter:
    Identifier ...

TemplateParameterList의 마지막 템플릿 매개변수가 TemplateSequenceParameter로 선언되면, 그것은 0개 이상의 뒤따르는 템플릿 인자와 일치해요. TemplateAliasParameter에 전달될 수 있는 어떤 인자든 시퀀스 매개변수에 전달될 수 있어요.

그런 인자 시퀀스 자체는 템플릿 밖에서 사용하기 위해 앨리어스될 수 있어요. std.meta.AliasSeq 템플릿은 단순히 자신의 시퀀스 매개변수를 앨리어스해요:

alias AliasSeq(Args...) = Args;

이제 명확성을 위해 TemplateSequenceParameter를 그 이름으로 부를 거예요. AliasSeq 자체는 타입, 값, 심볼이 아니에요. 그것은 타입, 값, 심볼의 어떤 혼합(또는 없는 것)도 될 수 있는 컴파일 타임 시퀀스예요.

AliasSeq의 요소들은 선언이나 표현식에서 참조될 때 자동으로 확장돼요. AliasSeq는 템플릿을 인스턴스화하는 인자로 사용될 수 있어요.

동질 시퀀스 (Homogeneous Sequences)
  • 요소들이 전적으로 타입으로 구성된 AliasSeq를 타입 시퀀스 또는 TypeSeq라 불러요.
  • 요소들이 전적으로 값으로 구성된 AliasSeq를 값 시퀀스 또는 ValueSeq라 불러요.
  • typeof를 ValueSeq에 사용해 TypeSeq를 얻을 수 있어요.

ValueSeq는 함수를 호출하는 인자로 사용될 수 있어요:

import std.stdio : writeln;

template print(args...) // args must be a ValueSeq
{
    void print()
    {
        writeln("args are ", args);
    }
}

void main()
{
    print!(1, 'a', 6.8)(); // prints: args are 1a6.8
}

TypeSeq는 함수의 매개변수 시퀀스를 선언하는 데 사용될 수 있어요:

import std.stdio : writeln;

template print(Types...) // Types must be a TypeSeq
{
    void print(Types args) // args is a ValueSeq
    {
        writeln("args are ", args);
    }
}

void main()
{
    print!(int, char, double)(1, 'a', 6.8); // prints: args are 1a6.8
}

값 시퀀스(value sequence)는:

  • 복사될 수 있음
  • 주소를 취할 수 없음
  • 함수에서 반환될 수 없음 — 대신 std.typecons.Tuple을 반환하세요
lvalue 시퀀스 (Lvalue Sequences)

TypeSeq는 변수를 선언하는 데에도 사용될 수 있어요. 타입이 TypeSeq인 변수를 lvalue 시퀀스라 불러요.

  • lvalue 시퀀스의 요소는 수정 가능할 수 있어요.
  • lvalue 시퀀스는 요소들이 호환되는 값 시퀀스로부터 초기화, 할당, 비교될 수 있어요.
import std.meta: AliasSeq;
// use a type alias just for convenience
alias TS = AliasSeq!(string, int);

TS tup; // lvalue sequence
assert(tup == AliasSeq!("", 0)); // TS.init
// elements can be modified
tup[1]++;
assert(tup[1] == 1);

byte i = 5;
// initialize another lvalue sequence from a sequence of a value and a symbol
auto tup2 = AliasSeq!("hi", i); // value of i is copied
i++;
assert(tup2[1] == 5); // unchanged

enum hi5 = AliasSeq!("hi", 5); // rvalue sequence
static assert(is(typeof(hi5) == TS));
// compare elements
assert(tup2 == hi5); // OK, byte and int have a common type

// lvalue sequence can be assigned to a ValueSeq
tup = tup2;
assert(tup == hi5);
  • .tupleof를 class나 struct 인스턴스에 사용해 그 필드들의 lvalue 시퀀스를 얻을 수 있어요.
  • .tupleof를 정적 배열 인스턴스에 사용해 그 요소들의 lvalue 시퀀스를 얻을 수 있어요.

lvalue 시퀀스는 단일 표현식에서 초기화될 수 있어요. 각 요소는 주어진 표현식으로 초기화돼요.

import std.meta: AliasSeq;
AliasSeq!(int, int, int) vs = 4;
assert(vs == AliasSeq!(4, 4, 4));

int[3] sa = [1, 2, 3];
vs = sa.tupleof;
assert(vs == AliasSeq!(1, 2, 3));
시퀀스 연산 (Sequence Operations)
  • AliasSeq의 요소 수는 .length 속성으로 가져올 수 있어요.
  • n번째 요소는 Seq[n]으로 인덱싱하여 가져올 수 있어요. 인덱스는 컴파일 타임에 알려져야 해요. 요소가 변수로 해석되는 심볼이거나 시퀀스가 lvalue 시퀀스일 때 결과는 lvalue예요.
  • 슬라이싱(slicing)은 원본 시퀀스의 요소 부분집합을 가진 새 시퀀스를 생성해요.
import std.meta : AliasSeq;

int v = 4;
// alias a sequence of 3 values and one symbol
alias nums = AliasSeq!(1, 2, 3, v);
static assert(nums.length == 4);
static assert(nums[1] == 2);

//nums[0]++; // Error, nums[0] is an rvalue
nums[3]++; // OK, nums[3] is bound to v, an lvalue
assert(v == 5);

// slice first 3 elements
alias trio = nums[0 .. $-1];
// expand into an array literal
static assert([trio] == [1, 2, 3]);

AliasSeq는 정적 컴파일 타임 엔티티이므로, 컴파일 타임이나 런타임에 요소를 동적으로 변경·추가·제거할 방법이 없어요. 대신:

  • 원본 시퀀스(또는 그것의 슬라이스)와 그 앞뒤에 추가 요소를 사용해 새 시퀀스를 구성하세요.
  • Alias Assignment를 사용해 새 시퀀스를 반복적으로 구성하세요.

시퀀스는 foreach 문장을 사용해 각 요소에 대한 코드를 '언롤(unroll)'할 수 있어요.

타입 시퀀스 추론 (Type Sequence Deduction)

타입 시퀀스는 암시적으로 인스턴스화된 함수 템플릿의 뒤따르는 매개변수로부터 추론될 수 있어요:

import std.stdio;

template print(T, Args...)
{
    void print(T first, Args args)
    {
        writeln(first);
        static if (args.length) // if more arguments
            print(args);        // recurse for remaining arguments
    }
}

void main()
{
    // Calls `print` with:
    // T = int, Args = (char, double)
    // T = char, Args = (double)
    // T = double, Args = ()
    print(1, 'a', 6.8);
}

출력:

1
a
6.8

타입 시퀀스는 함수 인자로 전달된 함수 포인터나 delegate의 매개변수 목록에서도 추론될 수 있어요:

size_t arity(R, Args...)(R function(Args) fp) => Args.length;

int f(int);
void g(string, Object);

static assert(arity((){}) == 0); // R = void, Args = ()
static assert(arity(&f) == 1);   // R = int, Args = (int)
static assert(arity(&g) == 2);   // R = void, Args = (string, Object)

추론은 매개변수 목록을 부분적으로 일치시킬 수 있어요:

/* Partially applies a delegate by tying its first argument to a particular value.
 * R = return type
 * T = first argument type
 * Args = TypeSeq of remaining argument types
 */
R delegate(Args) partial(R, T, Args...)(R function(T, Args) dg, T first)
{
    // return a closure
    return (Args args) => dg(first, args);
}

int plus(int x, int y, int z) => x + y + z;

void main()
{
    auto plus_two = partial(&plus, 2); // R = int, T = int, Args = (int, int)
    assert(plus_two(6, 8) == 16);
}
특수화 (Specialization)

시퀀스 매개변수를 가진 템플릿과 시퀀스 매개변수가 없는 템플릿이 둘 다 템플릿 인스턴스화에 정확히 일치하면, TemplateSequenceParameter가 없는 템플릿이 선택돼요.

template Foo(T)         { pragma(msg, "1"); }   // #1
template Foo(int n)     { pragma(msg, "2"); }   // #2
template Foo(alias sym) { pragma(msg, "3"); }   // #3
template Foo(Args...)   { pragma(msg, "4"); }   // #4

import std.stdio;

// Any sole template argument will never match to #4
alias foo1 = Foo!(int);          // instantiates #1
alias foo2 = Foo!(3);            // instantiates #2
alias foo3 = Foo!(std);          // instantiates #3

alias foo4 = Foo!(int, 3, std);  // instantiates #4

기본 인자 (Default Arguments)

뒤따르는 템플릿 매개변수들에 기본 인자를 줄 수 있어요:

template Foo(T, U = int) { ... }
Foo!(uint,long); // instantiate Foo with T as uint, and U as long
Foo!(uint);      // instantiate Foo with T as uint, and U as int

template Foo(T, U = T*) { ... }
Foo!(uint);      // instantiate Foo with T as uint, and U as uint*

에포니머스 템플릿 (Eponymous Templates)

템플릿이 템플릿 식별자와 같은 이름의 멤버를 포함하면, 그 멤버들은 템플릿 인스턴스화에서 참조되는 것으로 가정돼요:

template foo(T)
{
    T foo; // declare variable foo of type T
}

void main()
{
    foo!(int) = 6; // instead of foo!(int).foo
}

다음 예제는 에포니머스 멤버가 하나보다 많고 Implicit Function Template Instantiation을 사용해요:

template foo(S, T)
{
    // each member contains all the template parameters
    void foo(S s, T t) {}
    void foo(S s, T t, string) {}
}

void main()
{
    foo(1, 2, "test"); // foo!(int, int).foo(1, 2, "test")
    foo(1, 2); // foo!(int, int).foo(1, 2)
}

애그리게이트 타입 템플릿 (Aggregate Type Templates)

ClassTemplateDeclaration:
    class Identifier TemplateParameters ;
    class Identifier TemplateParameters Constraintopt BaseClassListopt AggregateBody
    class Identifier TemplateParameters BaseClassListopt Constraintopt AggregateBody

InterfaceTemplateDeclaration:
    interface Identifier TemplateParameters ;
    interface Identifier TemplateParameters Constraintopt BaseInterfaceListopt AggregateBody
    interface Identifier TemplateParameters BaseInterfaceList Constraint AggregateBody

StructTemplateDeclaration:
    struct Identifier TemplateParameters ;
    struct Identifier TemplateParameters Constraintopt AggregateBody

UnionTemplateDeclaration:
    union Identifier TemplateParameters ;
    union Identifier TemplateParameters Constraintopt AggregateBody

템플릿이 정확히 하나의 멤버를 선언하고, 그 멤버가 템플릿과 같은 이름의 클래스라면 (Eponymous Templates 참조):

template Bar(T)
{
    class Bar
    {
        T member;
    }
}

그러면 ClassTemplateDeclaration이라 불리는 의미적 등가물을 다음과 같이 쓸 수 있어요:

class Bar(T)
{
    T member;
}

struct, union, interface도 템플릿 매개변수 목록을 제공함으로써 템플릿으로 변환될 수 있어요.

함수 템플릿 (Function Templates)

템플릿이 정확히 하나의 멤버를 선언하고, 그 멤버가 템플릿과 같은 이름의 함수라면, 그것은 함수 템플릿 선언(function template declaration)이에요. 또는, 함수 템플릿 선언은 Parameters 바로 앞에 TemplateParameterList가 있는 함수 선언이에요.

타입 T의 제곱을 계산하는 함수 템플릿은:

T square(T)(T t)
{
    return t * t;
}

이것은 다음으로 낮아져요(lowered):

template square(T)
{
    T square(T t)
    {
        return t * t;
    }
}

함수 템플릿은 Identifier!(TemplateArgumentList)로 명시적으로 인스턴스화될 수 있어요:

writefln("The square of %s is %s", 3, square!(int)(3));

암시적 함수 템플릿 인스턴스화 (IFTI)

함수 템플릿은 TemplateArgumentList가 함수 인자의 타입에서 추론될 수 있으면 암시적으로 인스턴스화될 수 있어요:

T square(T)(T t)
{
    return t * t;
}

writefln("The square of %s is %s", 3, square(3));  // T is deduced to be int

타입 매개변수 추론은 함수 인자의 순서에 영향을 받지 않아요. TemplateArgumentList에 TemplateParameterList의 매개변수보다 적은 인자가 공급되면, 인자들은 왼쪽에서 오른쪽으로 매개변수를 채우고, 나머지 매개변수들은 함수 인자에서 추론돼요.

제한 (Restrictions)

암시적으로 추론될 함수 템플릿 타입 매개변수는 최소한 하나의 함수 매개변수의 타입에 나타나야 해요:

void foo(T : U*, U)(U t) {}

void main()
{
    int x;
    foo!(int*)(x);   // ok, U is deduced and T is specified explicitly
    //foo(x);        // error, only U can be deduced, not T
}

템플릿 매개변수가 추론되어야 할 때, 에포니머스 멤버들은 추론이 멤버들이 사용되는 방식에 의존하므로 static if 조건에 의존할 수 없어요:

template foo(T)
{
    static if (is(T)) // T is not yet known...
        void foo(T t) {} // T is deduced from the member usage
}

void main()
{
    //foo(0); // Error: cannot deduce function from argument types
    foo!int(0); // Ok since no deduction necessary
}

IFTI는 매개변수 타입이 alias 템플릿 인스턴스일 때 작동하지 않아요:

struct S(T) {}
alias A(T) = S!T;
void f(T)(A!T) {}

void main()
{
    A!int v;
    //f(v); // error
    f!int(v); // OK
}

TypeSeq 템플릿 매개변수는 함수 인자에서 추론될 수 있어요.

타입 변환 (Type Conversions)

템플릿 타입 매개변수가 함수 인자의 리터럴 표현식과 일치하면, 추론된 타입은 그것들의 축소 변환(narrowing conversions)을 고려할 수 있어요.

void foo(T)(T v)        { pragma(msg, "in foo, T = ", T); }
void bar(T)(T v, T[] a) { pragma(msg, "in bar, T = ", T); }

void main()
{
    foo(1);
    // an integer literal type is analyzed as int by default
    // then T is deduced to int

    short[] arr;
    bar(1, arr);
    // arr is short[], and the integer literal 1 is
    // implicitly convertible to short.
    // then T will be deduced to short.

    bar(1, [2.0, 3.0]);
    // the array literal is analyzed as double[],
    // and the integer literal 1 is implicitly convertible to double.
    // then T will be deduced to double.
}

동적 배열과 포인터 인자에 대한 추론된 타입 매개변수는 비한정 헤드(unqualified head)를 가져요:

void foo(T)(T arg) { pragma(msg, T); }

void test()
{
    int[] marr;
    const(int[]) carr;
    immutable(int[]) iarr;
    foo(marr);  // T == int[]
    foo(carr);  // T == const(int)[]
    foo(iarr);  // T == immutable(int)[]

    int* mptr;
    const(int*) cptr;
    immutable(int*) iptr;
    foo(mptr);  // T == int*
    foo(cptr);  // T == const(int)*
    foo(iptr);  // T == immutable(int)*
}

반환 타입 추론 (Return Type Deduction)

함수 템플릿은 일반 함수처럼 함수 안의 ReturnStatement에 기반하여 반환 타입이 추론될 수 있어요.

auto square(T)(T t)
{
    return t * t;
}

auto i = square(2);
static assert(is(typeof(i) == int));

Auto Ref 매개변수 (Auto Ref Parameters)

템플릿 함수는 auto ref 매개변수를 가질 수 있어요. auto ref 매개변수는 해당 인자가 lvalue이면 ref 매개변수가 되고, 그렇지 않으면 값 매개변수가 돼요:

int countRefs(Args...)(auto ref Args args)
{
    int result;

    foreach (i, _; args)
    {
        if (__traits(isRef, args[i]))
            result++;
    }
    return result;
}

void main()
{
    int y;
    assert(countRefs(3, 4) == 0);
    assert(countRefs(3, y, 4) == 1);
    assert(countRefs(y, 6, y) == 2);
}

Auto ref 매개변수는 auto ref 반환 속성과 결합될 수 있어요:

auto ref min(T, U)(auto ref T lhs, auto ref U rhs)
{
    return lhs > rhs ? rhs : lhs;
}

void main()
{
    int i;
    i = min(4, 3);
    assert(i == 3);

    int x = 7, y = 8;
    i = min(x, y);
    assert(i == 7);
    // result is an lvalue
    min(x, y) = 10;    // sets x to 10
    assert(x == 10 && y == 8);

    static assert(!__traits(compiles, min(3, y) = 10));
    static assert(!__traits(compiles, min(y, 3) = 10));
}

기본 인자 (Default Arguments)

암시적으로 추론되지 않는 템플릿 인자는 기본 값을 가질 수 있어요:

void foo(T, U=T*)(T t) { U p; ... }

int x;
foo(x);    // T is int, U is int*

가변 함수 템플릿(Variadic Function Templates)은 기본 값을 가진 매개변수를 가질 수 있어요. 이 매개변수들은 IFTI의 경우 항상 기본 값으로 설정돼요.

size_t fun(T...)(T t, string file = __FILE__)
{
    import std.stdio;
    writeln(file, " ", t);
    return T.length;
}

assert(fun(1, "foo") == 2);  // uses IFTI
assert(fun!int(1, "filename") == 1);  // no IFTI

템플릿 생성자 (Template Constructors)

ConstructorTemplate:
    this TemplateParameters Parameters MemberFunctionAttributesopt Constraintopt FunctionBody
    this TemplateParameters Parameters MemberFunctionAttributesopt Constraintopt MissingFunctionBody

템플릿은 class와 struct의 생성자를 형성하는 데 사용될 수 있어요.

Enum & 변수 템플릿 (Enum & Variable Templates)

애그리게이트와 함수처럼, 변수 선언과 매니페스트 상수(manifest constants)는 Initializer가 있으면 템플릿 매개변수를 가질 수 있어요:

enum bool within(alias v, T) = v <= T.max && v >= T.min;
ubyte[T.sizeof] storage(T) = 0;
const triplet(alias v) = [v, v+1, v+2];

static assert(within!(-128F, byte));
static assert(storage!(int[2]).length == 8);
static assert(triplet!3 == [3, 4, 5]);

그 선언들은 다음 TemplateDeclaration들로 변환돼요:

template within(alias v, T)
{
    enum bool within = v <= T.max && v >= T.min;
}
template storage(T)
{
    ubyte[T.sizeof] storage = 0;
}
template triplet(alias v)
{
    const triplet = [v, v+1, v+2];
}

Alias 템플릿 (Alias Templates)

AliasDeclaration도 선택적 템플릿 매개변수를 가질 수 있어요:

alias ElementType(T : T[]) = T;
alias Sequence(TL...) = TL;

이것은 다음으로 낮아져요:

template ElementType(T : T[])
{
    alias ElementType = T;
}
template Sequence(TL...)
{
    alias Sequence = TL;
}

중첩 템플릿 (Nested Templates)

템플릿이 애그리게이트나 함수 지역 스코프에서 선언되면, 인스턴스화된 함수들은 둘러싼 스코프의 컨텍스트를 암시적으로 캡처해요.

class C
{
    int num;

    this(int n) { num = n; }

    template Foo()
    {
        // 'foo' can access 'this' reference of class C object.
        void foo(int n) { this.num = n; }
    }
}

void main()
{
    auto c = new C(1);
    assert(c.num == 1);

    c.Foo!().foo(5);
    assert(c.num == 5);

    template Bar()
    {
        // 'bar' can access local variable of 'main' function.
        void bar(int n) { c.num = n; }
    }
    Bar!().bar(10);
    assert(c.num == 10);
}

위에서 Foo!().foo는 class C의 final 멤버 함수와 똑같이 동작하고, Bar!().bar는 함수 main() 안의 중첩 함수와 똑같이 동작해요.

애그리게이트 타입 제한 (Aggregate Type Limitations)

중첩 템플릿은 애그리게이트 타입에 비정적 필드를 추가할 수 없어요. 중첩 템플릿에서 선언된 필드는 암시적으로 static이 돼요. 중첩 템플릿은 class나 interface에 가상 함수를 추가할 수 없어요. 중첩 템플릿 안의 메서드는 암시적으로 final이 돼요.

class Foo
{
    template TBar(T)
    {
        T xx;           // becomes a static field of Foo
        void func(T) {} // implicitly final
        //abstract void baz(); // error, final functions cannot be abstract

        static T yy;                    // Ok
        static void func(T t, int y) {} // Ok
    }
}

void main()
{
    alias bar = Foo.TBar!int;
    bar.xx++;
    //bar.func(1); // error, no this

    auto o = new Foo;
    o.TBar!int.func(1); // OK
}

암시적 중첩 (Implicit Nesting)

템플릿에 템플릿 alias 매개변수가 있고 지역 심볼로 인스턴스화되면, 인스턴스화된 함수가 주어진 지역 심볼의 런타임 데이터에 접근하기 위해 암시적으로 중첩될 거예요.

template Foo(alias sym)
{
    void foo() { sym = 10; }
}

class C
{
    int num;

    this(int n) { num = n; }

    void main()
    {
        assert(this.num == 1);

        alias fooX = Foo!(C.num).foo;

        // fooX will become member function implicitly, so &fooX
        //     returns a delegate object.
        static assert(is(typeof(&fooX) == delegate));

        fooX(); // called by using valid 'this' reference.
        assert(this.num == 10);  // OK
    }
}

void main()
{
    new C(1).main();

    int num;
    alias fooX = Foo!num.foo;

    // fooX will become nested function implicitly, so &fooX
    //     returns a delegate object.
    static assert(is(typeof(&fooX) == delegate));

    fooX();
    assert(num == 10);  // OK
}

함수뿐 아니라, 인스턴스화된 class와 struct 타입도 암시적으로 캡처된 컨텍스트를 통해 중첩될 수 있어요.

class C
{
    int num;
    this(int n) { num = n; }

    class N(T)
    {
        // instantiated class N!T can become nested in C
        T foo() { return num * 2; }
    }
}

void main()
{
    auto c = new C(10);
    auto n = c.new N!int();
    assert(n.foo() == 20);
}
void main()
{
    int num = 10;
    struct S(T)
    {
        // instantiated struct S!T can become nested in main()
        T foo() { return num * 2; }
    }
    S!int s;
    assert(s.foo() == 20);
}

템플릿 struct는 앨리어스된 인자로 전달된 지역 심볼로 인스턴스화되면 중첩 struct가 될 수 있어요:

struct A(alias F)
{
    int fun(int i) { return F(i); }
}

A!F makeA(alias F)() { return A!F(); }

void main()
{
    int x = 40;
    int fun(int i) { return x + i; }
    A!fun a = makeA!fun();
    assert(a.fun(2) == 42);
}

컨텍스트 제한 (Context Limitation)

현재 중첩 템플릿은 최대 한 개의 컨텍스트를 캡처할 수 있어요. 전형적인 예로, 비정적 템플릿 멤버 함수는 템플릿 alias 매개변수로 지역 심볼을 받을 수 없어요.

class C
{
    int num;
    void foo(alias sym)() { num = sym * 2; }
}

void main()
{
    auto c = new C();
    int var = 10;
    c.foo!var();    // NG, foo!var requires two contexts, 'this' and 'main()'
}

하지만, 한 컨텍스트가 다른 컨텍스트에서 간접적으로 접근 가능하면 허용돼요.

int sum(alias x, alias y)() { return x + y; }

void main()
{
    int a = 10;
    void nested()
    {
        int b = 20;
        assert(sum!(a, b)() == 30);
    }
    nested();
}

두 지역 변수 ab는 서로 다른 컨텍스트에 있지만, 바깥 컨텍스트가 안쪽 컨텍스트에서 간접적으로 접근 가능하므로, 중첩 템플릿 인스턴스 sum!(a, b)는 안쪽 컨텍스트만 캡처해요.

재귀 템플릿 (Recursive Templates)

템플릿 기능들을 결합해 비자명한 함수의 컴파일 타임 평가 같은 흥미로운 효과를 만들 수 있어요. 예를 들어, 팩토리얼 템플릿은 다음과 같이 쓸 수 있어요:

template factorial(int n)
{
    static if (n == 1)
        enum factorial = 1;
    else
        enum factorial = n * factorial!(n - 1);
}

static assert(factorial!(4) == 24);

더 많은 정보와 CTFE (Compile-time Function Execution) 팩토리얼 대안은 Template Recursion을 참조하세요.

템플릿 제약 (Template Constraints)

Constraint:
    if ( Expression )

제약(constraints)은 TemplateParameterList에서 가능한 것 이상으로 템플릿에 인자를 일치시키는 데 추가 제약을 부과하는 데 사용돼요. Expression은 컴파일 타임에 계산되고 boolean 값으로 변환되는 결과를 반환해요. 그 값이 true면 템플릿이 일치되고, 그렇지 않으면 일치하지 않아요.

예를 들어, 다음 함수 템플릿은 N의 홀수 값만 일치해요:

void foo(int N)()
    if (N & 1)
{
    ...
}
...
foo!(3)();  // OK, matches
foo!(4)();  // Error, no match

템플릿 제약은 애그리게이트 타입(struct, class, union)과 함께 사용될 수 있어요. 제약은 라이브러리 모듈 std.traits와 함께 효과적으로 사용돼요:

import std.traits;

struct Bar(T)
    if (isIntegral!T)
{
    ...
}
...
auto x = Bar!int;       // OK, int is an integral type
auto y = Bar!double;    // Error, double does not satisfy constraint

더 알아보기