템플릿
템플릿 (Templates)
D 언어의 템플릿은 제네릭 프로그래밍(generic programming)을 가능하게 해주는 핵심 기능이에요. 타입·값·심볼·시퀀스를 매개변수로 받아서, 같은 논리를 여러 타입에 재사용할 수 있게 만들어 주죠. 이번 장에서는 템플릿을 어떻게 선언하고, 어떤 종류의 매개변수가 있고, 인스턴스화가 어떤 식으로 일어나는지 차근차근 살펴볼게요.
본문
템플릿 선언 (Template Declarations)
템플릿은 D가 제네릭 프로그래밍에 접근하는 방식이에요. 템플릿은 TemplateDeclaration로 정의할 수 있습니다.
TemplateDeclaration:
template Identifier TemplateParameters Constraintopt { DeclDefsopt }
TemplateParameters:
( TemplateParameterListopt )
TemplateParameterList:
TemplateParameter
TemplateParameter ,
TemplateParameter , TemplateParameterList
DeclDefs는 템플릿 바디 안에 들어갈 선언들이에요. 가장 단순한 형태부터 볼게요.
템플릿 매개변수에 대해 알아보기 전에, 가장 기본적인 템플릿 하나를 만들어볼게요.
template t(T) // declare type parameter T
{
T v; // declare a member variable of type T within template t
}
이렇게 하면 타입 매개변수 T를 선언하고, 그 타입의 멤버 변수 v를 템플릿 안에 두는 거예요.
같은 Identifier(이름)를 가진 템플릿이 여러 개 있으면, 이 경우의 규칙이 조금 달라져요.
템플릿에 템플릿과 같은 식별자를 가진 멤버가 있다면, 그 템플릿은 Eponymous Template이 됩니다. 또 template은 짧은 문법으로도 쓸 수 있어요.
템플릿 인스턴스화 (Template Instantiation)
템플릿은 사용하기 전에 반드시 인스턴스화(instantiation)해야 해요. 즉, 템플릿에 인자 리스트를 넘겨주는 거죠. 이 인자들은 보통 템플릿 바디 안으로 치환되어서, 새로운 스코프를 가진 엔티티가 됩니다.
함수 템플릿에 대해서는 나중에 다룰게요.
명시적 템플릿 인스턴스화 (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
일단 인스턴스화되고 나면, 템플릿 안의 선언들(이걸 템플릿 멤버라고 불러요)은 TemplateInstance의 스코프 안에 위치하게 됩니다.
template TFoo(T) { alias Ptr = T*; }
...
TFoo!(int).Ptr x; // declare x to be of type int*
그래서 TFoo!(int)의 멤버 Ptr을 꺼내 쓰려면 위처럼 .Ptr으로 접근하는 거예요.
만약 TemplateArgument가 단일 인자라면, 괄호를 생략할 수도 있어요.
TFoo!int.Ptr x; // same as TFoo!(int).Ptr x;
템플릿 인스턴스화는 별칭(alias)으로 만들 수도 있습니다.
template TFoo(T) { alias Ptr = T*; }
alias foo = TFoo!(int);
foo.Ptr x; // declare x to be of type int*
공통 인스턴스화 (Common Instantiation)
같은 TemplateDeclaration과 같은 TemplateArgumentList로 만들어진 여러 인스턴스화는, 사실 모두 동일한 템플릿 인스턴스를 가리켜요.
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)));
여기서 핵심은 이거예요. 3과 2+1은 둘 다 int 타입의 값 3이라서 같은 인스턴스이고, 3u도 int로 암묵 변환되어 같은 인스턴스를 가리킨다는 거죠.
실용 예제 (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이 템플릿 멤버 함수를 이름으로 찾을 때, 인스턴스화가 일어난 지점(instantiation point)의 스코프가 아니라, 템플릿이 정의된 모듈의 스코프를 사용합니다.
예시:
template TFoo(T) { void bar() { func(); } }
위 템플릿을 a라는 모듈에서 인스턴스화하면, func는 모듈 a 쪽이 아니라 템플릿을 정의한 쪽의 스코프에서 찾아요.
import a;
void func() { }
alias f = TFoo!(int); // error: func not defined in module a
왜 에러가 날까요? func가 지금 있는 파일(모듈 a를 임포트한 쪽)에는 정의되어 있지만, 템플릿 내부는 자기 모듈 스코프에서 해석되기 때문이에요. 다르게 표현하면, 템플릿 멤버가 참조하는 이름은 템플릿이 정의된 모듈에서 찾습니다.
예시:
template TFoo(T) { void bar() { func(1); } }
void func(double d) { }
import a;
void func(int i) { }
alias f = TFoo!(int);
...
f.bar(); // will call a.func(double)
여기서 f.bar()는 a.func(double)을 호출해요. 템플릿이 정의된 모듈에는 func(double)이 있고, 그게 승리하기 때문이죠. 로컬에 func(int)가 있어도 템플릿 인스턴스화 지점의 스코프로 해석되지 않는다는 걸 꼭 기억하세요.
반면 TemplateParameter는 인스턴스화 지점에서 해석됩니다. 매개변수 자체는 TemplateDeclaration이 아니라, 실제로 인자를 넘기는 지점의 스코프에서 결정되는 거죠.
템플릿 매개변수 (Template Parameters)
TemplateParameter:
TemplateTypeParameter
TemplateValueParameter
TemplateAliasParameter
TemplateSequenceParameter
TemplateThisParameter
템플릿 매개변수는 타입, 값, 심볼, 또는 시퀀스를 받아요.
- 타입 매개변수는 어떤 타입이든 받을 수 있어요.
- 값 매개변수는 컴파일 타임에 정적으로 평가될 수 있는 표현식이면 받을 수 있어요.
- 별칭(alias) 매개변수는 거의 모든 심볼을 받을 수 있어요.
- 시퀀스 매개변수는 0개 이상의 타입·값·심볼을 받을 수 있어요.
템플릿 매개변수는 specialization(특수화)을 가질 수 있어요. 특수화는 매개변수의 종류에 따라 해당 TemplateParameter가 매치할 인자를 제한합니다.
template t(T : int) // type T must implicitly convert to int
{
...
}
이렇게 하면 T는 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의 타입에 가장 잘 맞는 것 중에서 가장 특수화된 템플릿이에요. 예를 들어 TFoo!(double[])은 T[] 패턴(여기선 T가 double)이 맞는 #2가 선택되죠.
타입 매개변수 추론 (Type Parameter Deduction)
템플릿 매개변수의 타입은, 템플릿 인자를 해당 템플릿 매개변수와 비교해서 특정 인스턴스화에 대해 추론돼요.
각 매개변수에 대해 다음 규칙을 순서대로 적용해서 타입이 추론됩니다.
- 매개변수에 타입 특수화가 없으면, 매개변수의 타입은 템플릿 인자로 설정돼요.
- 타입 특수화가 어떤 타입 매개변수에 의존하면, 그 매개변수의 타입은 타입 인자의 대응하는 부분으로 설정돼요.
- 모든 타입 인자를 살펴본 뒤에도 타입이 할당되지 않은 타입 매개변수가 남아 있으면, TemplateArgumentList에서 같은 위치에 있는 템플릿 인자에 대응하는 타입을 할당해요.
- 위 규칙을 적용해도 각 템플릿 매개변수에 정확히 하나의 타입이 나오지 않으면 에러예요.
예를 들어볼게요.
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
여기선 T[U] 패턴에 int[long]이 매치되면서 T=int, U=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
B가 A를 상속하므로 TFoo!(B)는 T : A 특수화(#규칙 (3))를 만족하는 거예요.
this 매개변수 (This Parameters)
TemplateThisParameter:
this TemplateTypeParameter
TemplateThisParameter는 함수나 메서드 안에서 this의 타입을 추론해 주는 특별한 장치예요. 보통 template this parameter라고 부르는 이 기능은, this의 타입이 const인지 immutable인지 mutable인지에 따라 서로 다른 코드가 인스턴스화되게 합니다. 즉 this가 const, immutable, 가변 중 어떤 것인지에 따라 템플릿이 각각 다른 인스턴스를 만들어 내는 거죠.
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)
즉 this T가 const(S), S, immutable(S)로 각각 추론되는 거예요. 같은 메서드가 this의 변경 가능성에 따라 다르게 동작할 수 있다는 뜻입니다.
런타임 타입 검사 피하기 (Avoiding Runtime Type Checks)
TemplateThisParameter를 쓰면, 런타임에 this의 타입을 확인하는 비용을 피할 수 있어요. 예를 들어 인터페이스 반환 타입 때문에 컴파일이 막히는 상황을 볼게요.
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는 auto라서 실제 반환 타입이 this의 타입, 즉 List!int가 되어야 하지만, 컴파일러가 그걸 확정하지 못해 Addable!int로 보게 됩니다. 그래서 remove를 못 찾고 에러가 나요.
그렇다면 this의 실제 타입을 T로 잡아주면 어떨까요? 아래처럼 template this parameter와 cast를 함께 쓰면 안전하게 해결돼요.
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));
}
add(this R)에서 R이 this의 실제 타입으로 추론되므로 list.add(1)의 타입이 List!int로 확정돼서 remove를 호출할 수 있게 됐어요. cast(R)this는 타입이 확실할 때 쓰는 안전한 캐스트예요.
값 매개변수 (Value Parameters)
TemplateValueParameter:
BasicType Declarator TemplateValueParameterSpecializationopt TemplateValueParameterDefaultopt
TemplateValueParameterSpecialization:
: ConditionalExpression
TemplateValueParameterDefault:
= AssignExpression
= SpecialKeyword
템플릿 값 매개변수는 컴파일 타임에 정적으로 평가될 수 있는 표현식을 인자로 받아요. 구체적으로는 다음 중 하나가 될 수 있습니다.
boolnull- 각 요소가 유효한 템플릿 값 인자가 될 수 있는 배열 리터럴
- 키와 값이 각각 유효한 템플릿 값 인자가 될 수 있는 연관 배열(associative array) 리터럴
- 각 인자가 유효한 템플릿 값 인자가 될 수 있는 구조체 리터럴
template foo(string s)
{
enum string bar = s ~ " betty";
}
void main()
{
import std.stdio;
writeln(foo!("hello").bar); // prints: hello betty
}
여기서는 문자열 "hello"를 값 매개변수 s로 넘겨서, 컴파일 타임에 "hello betty"라는 문자열을 만든 거예요.
값 매개변수 특수화 (Specialization)
제공되는 특수화나 기본 표현식은 컴파일 타임에 평가 가능해야 해요.
이 예제에서 템플릿 foo는 int 타입 U와 값이 정확히 10인 int v를 요구해요.
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)));
}
foo!(int, 11)처럼 값이 10이 아니면 컴파일조차 되지 않아요. 이는 특정 값에 대해 서로 다른 템플릿 바디가 필요할 때 유용합니다. 다른 정수 리터럴 값을 받도록 또 다른 템플릿 오버로드를 정의하면 되는 거죠.
별칭 매개변수 (Alias Parameters)
TemplateAliasParameter:
alias Identifier TemplateAliasParameterSpecializationopt TemplateAliasParameterDefaultopt
alias BasicType Declarator TemplateAliasParameterSpecializationopt TemplateAliasParameterDefaultopt
TemplateAliasParameterSpecialization:
: Type
: ConditionalExpression
TemplateAliasParameterDefault:
= Type
= ConditionalExpression
별칭(alias) 매개변수는 템플릿을 심볼 이름이나 컴파일 타임에 계산된 값으로 매개변수화할 수 있게 해줘요. 거의 모든 종류의 D 심볼을 쓸 수 있는데, 타입 이름, 전역 이름, 지역 이름, 모듈 이름, 템플릿 이름, 템플릿 인스턴스가 모두 해당돼요.
심볼 별칭 (Symbol Aliases)
먼저 타입 이름을 별칭으로 넘길 수 있어요. 예를 들어 타입 Foo를 넘겨서 그 정적 멤버 x에 접근하는 경우죠.
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);
}
전역 이름도 넘길 수 있어요. alias var에 전역 변수를 넘겨서 그 주소를 가리키는 거죠.
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);
}
지역 이름도 마찬가지로 넘길 수 있어요. 함수 안의 지역 변수를 별칭으로 넘겨서 참조하는 겁니다.
template Foo(alias var)
{
void inc() { var++; }
}
void main()
{
int v = 4;
alias foo = Foo!v;
foo.inc();
assert(v == 5);
}
여기서 Foo!v가 지역 변수 v를 별칭으로 잡고, inc()가 그 v를 1 증가시키는 거예요. (자세한 규칙은 Implicit Template Nesting을 참고하세요.)
모듈 이름도 별칭 매개변수로 넘길 수 있어요. 그 모듈의 함수를 호출하는 용도로 쓰는 거죠.
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");
}
std.conv 자체를 별칭으로 넘겨서 a.text로 그 모듈의 text 함수를 참조한 거예요.
템플릿 이름도 별칭으로 받을 수 있습니다. 아래처럼 템플릿을 넘겨서 그걸 다시 인자로 인스턴스화하는 패턴이에요.
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);
}
마지막으로 템플릿 인스턴스 이름도 넘길 수 있어요. 이미 만들어진 인스턴스의 멤버를 또 다른 별칭으로 가져오는 거죠.
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)
별칭 매개변수는 값도 받을 수 있어요. 먼저 리터럴을 넘기는 경우를 볼게요.
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
}
3과 "bar"라는 리터럴을 별칭으로 넘겨서 정적 멤버 i와 s를 채운 거예요.
컴파일 타임 값(상수 표현식이나 함수 호출 결과)도 넘길 수 있어요. 인자는 컴파일 타임에 평가됩니다.
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);
}
5 * two는 컴파일 타임에 10으로 평가되고, get10()도 컴파일 타임에 실행돼요. 그래서 bar는 실제로 foo와 같은 Foo!10 인스턴스가 되는 거예요.
함수 리터럴(람다)도 별칭 매개변수로 넘길 수 있습니다.
template Foo(alias fun)
{
enum val = fun(2);
}
alias foo = Foo!((int x) => x * x);
static assert(foo.val == 4);
(int x) => x * x라는 함수 리터럴을 넘겨서 컴파일 타임에 fun(2)를 계산한 거예요.
타입이 있는 별칭 매개변수 (Typed Alias Parameters)
별칭 매개변수에도 타입을 붙일 수 있어요. 이러면 해당 타입의 심볼만 받아들여집니다.
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
}
alias int p라고 했으므로 Foo!i(i가 int)는 통과하지만, Foo!f(f가 float)는 인스턴스화에 실패해요.
별칭 매개변수 특수화 (Specialization)
별칭 매개변수의 특수화는 매개변수가 매치할 인자를 제한해요. 특수화는 타입이거나 컴파일 타임 표현식일 수 있습니다.
타입 특수화: 예를 들어 alias s : S는 S 타입을 받고, const 같은 한정자(qualifier)는 무시해요.
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
별칭 매개변수는 리터럴과 사용자 정의 타입 심볼 둘 다 받을 수 있지만, 타입 매개변수나 값 매개변수에 대한 매치보다는 특수화 정도가 낮아요. 즉 다른 후보가 더 잘 맞으면 그쪽이 우선됩니다.
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
Foo!(S)은 타입이라 #1, Foo!(1)은 값이라 #2, 그리고 #2로 매치되지 않는 것들([1,2], var)은 별칭 #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
C!int는 U!V 패턴(여기선 U=C, V=int)에 매치되므로 더 특수화된 #5가 선택돼요.
시퀀스 매개변수 (Sequence Parameters)
TemplateSequenceParameter:
Identifier ...
TemplateParameterList의 마지막 템플릿 매개변수가 TemplateSequenceParameter이면, 그 매개변수는 임의의 개수(0개 이상)의 인자를 받을 수 있어요. 시퀀스 매개변수는 사실상 TemplateAliasParameter처럼 동작해서 여러 인자를 하나로 묶어줍니다.
이런 인자 시퀀스는 템플릿 밖에서도 쓰이도록 별칭으로 만들 수 있어요. std.meta.AliasSeq를 쓰면 됩니다.
alias AliasSeq(Args...) = Args;
한편 TemplateSequenceParameter로 만들어지는 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
}
여기서 Types가 (int, char, double)이므로, void print(Types args)는 void print(int, char, double)로 펼쳐져요.
값 시퀀스(value sequence)는 다음 성질이 있어요.
- 복사할 수 있어요.
- 주소를 취할 수 없어요.
- 함수에서 반환할 수 없어요. 대신
std.typecons.Tuple을 반환하세요.
lvalue 시퀀스 (Lvalue Sequences)
TypeSeq로 변수를 선언하면, 각 요소가 메모리에 실제 자리를 갖는 lvalue sequence가 돼요. 이건 값이 아니라 변경 가능한 변수라는 점이 중요합니다.
- 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);
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)
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는 요소 인덱싱이 가능하지만, 값 요소(nums[0])는 rvalue라 수정할 수 없어요. 반면 심볼 v에 묶인 nums[3]는 lvalue라서 ++가 가능하죠.
AliasSeq를 새로운 시퀀스로 만들 때는 다음 방법을 써요.
- 원래 시퀀스(또는 그 슬라이스)와 그 앞/뒤에 추가할 요소들을 사용해서 새 시퀀스를 구성할 수 있어요.
- Alias Assignment을 사용할 수도 있어요.
시퀀스는 foreach로 각 요소마다 코드를 '펼칠' 수 있어요.
타입 시퀀스 추론 (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
Args...가 남은 인자들을 타입 시퀀스로 받아서, 재귀적으로 하나씩 처리하는 전형적인 패턴이에요.
타입 시퀀스는 함수 포인터나 델리게이트의 매개변수 목록으로부터도 추론될 수 있어요.
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);
}
여기선 partial이 함수의 첫 인자를 특정 값에 고정한 클로저를 만들어요. (std.functional.partial도 함께 보세요: std.functional.partial)
시퀀스 매개변수 특수화 (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
단일 인자는 항상 더 구체적인 후보(#1~#3)가 잡아가므로 #4에 매치되지 않아요. 여러 인자(여기선 int, 3, std)를 넘길 때만 시퀀스가 선택됩니다.
기본 인자 (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*
기본 인자는 다른 매개변수를 참조할 수도 있어요. U = T*처럼요.
관련 내용으로 함수 템플릿 기본 인자도 함께 보세요.
Eponymous 템플릿 (Eponymous Templates)
템플릿 안에 템플릿 식별자와 같은 이름의 멤버가 있으면, 그 멤버가 템플릿 인스턴스화에서 참조된다고 간주됩니다.
template foo(T)
{
T foo; // declare variable foo of type T
}
void main()
{
foo!(int) = 6; // instead of foo!(int).foo
}
foo!(int) = 6이라고 쓰면 실제로는 foo!(int).foo = 6이라는 뜻이 돼요. 이 이름 짓기 규칙 때문에 템플릿이 마치 타입·함수처럼 보이게 되는 거죠.
다음 예제는 eponymous 멤버가 둘 이상이고, 암묵적 함수 템플릿 인스턴스화를 사용해요.
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
클래스·구조체·유니온·인터페이스도 템플릿이 될 수 있어요. 그런데 집계 타입 템플릿에는 **짧은 문법(short syntax)**이 있어요. 템플릿이 정확히 하나의 멤버만 선언하고, 그 멤버가 템플릿과 같은 이름의 클래스라면(아래 Eponymous Templates 참고), 아래 두 선언은 완전히 동일합니다.
template Bar(T)
{
class Bar
{
T member;
}
}
class Bar(T)
{
T member;
}
즉 class Bar(T)로 써도 내부적으로는 위 템플릿으로 변환되는 거예요. This Parameters도 함께 보세요.
클래스 템플릿과 마찬가지로, 구조체·유니온·인터페이스도 템플릿 매개변수 목록을 붙이면 템플릿으로 바꿀 수 있습니다.
함수 템플릿 (Function Templates)
템플릿이 정확히 하나의 멤버만 선언하고, 그 멤버가 템플릿과 같은 이름의 함수라면 그건 함수 템플릿 선언입니다. 또는 TemplateParameterList가 붙은 함수 선언(Parameters 바로 앞에)도 함수 템플릿 선언이에요.
타입 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
square(3)처럼 함수 인자만 넘기면 T가 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
}
T는 foo(x)로는 추론될 수 없어서 명시적으로 지정해야 해요. T가 함수 인자 타입에 나타나지 않기 때문이죠.
템플릿 매개변수가 추론되어야 할 때는, eponymous 멤버들이 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
}
foo(0)처럼 추론을 시키면 실패하고, foo!int(0)처럼 명시하면 성공해요.
IFTI는 매개변수 타입이 별칭 템플릿일 때는 동작하지 않아요.
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) 변환까지 고려할 수 있어요.
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.
}
bar(1, arr)에서는 1이 short로 축소 변환 가능하므로 T가 short로 추론되고, bar(1, [2.0, 3.0])에서는 double로 추론돼요.
동적 배열·포인터 인자에 대한 추론된 타입 매개변수는, 앞부분(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)*
}
carr(타입 const(int[]))에서 T는 const(int)[]로 추론되는데, 배열 전체의 head(int)는 한정자가 없는 상태로 유지돼요.
반환 타입 추론 (Return Type Deduction)
함수 템플릿은 ReturnStatement에 기반해 반환 타입이 추론될 수 있어요. 이는 Auto Functions와 같습니다.
auto square(T)(T t)
{
return t * t;
}
auto i = square(2);
static assert(is(typeof(i) == int));
auto square(T)(T t)의 반환 타입은 t * t의 타입에서 추론돼서, square(2)는 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);
}
countRefs(3, 4)는 두 인자 모두 rvalue라 ref가 아니므로 0, y가 들어가면 그 자리에선 ref라서 1, 둘 다 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));
}
min(x, y)은 두 인자가 모두 lvalue이므로 반환값이 lvalue가 되어서 min(x, y) = 10처럼 할당받을 수 있어요. 반면 min(3, y)처럼 한쪽이 rvalue면 결과가 lvalue가 아니라서 할당이 안 되죠.
함수 템플릿 기본 인자 (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
fun(1, "foo")은 IFTI를 사용해서 T = (int, string)로 2를 반환하고, fun!int(1, "filename")는 T = int 하나만이라 1을 반환해요.
템플릿 생성자 (Template Constructors)
ConstructorTemplate:
this TemplateParameters Parameters MemberFunctionAttributesopt Constraintopt FunctionBody
this TemplateParameters Parameters MemberFunctionAttributesopt Constraintopt MissingFunctionBody
템플릿은 클래스와 구조체의 생성자를 만드는 데도 사용될 수 있어요.
Enum 및 변수 템플릿 (Enum & Variable Templates)
집계 타입과 함수처럼, 변수 선언도 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 Templates)
AliasDeclaration에도 템플릿 매개변수를 붙일 수 있어요.
alias ElementType(T : T[]) = T;
alias Sequence(TL...) = TL;
이것들은 다음과 같이 변환됩니다.
template ElementType(T : T[])
{
alias ElementType = T;
}
template Sequence(TL...)
{
alias Sequence = TL;
}
alias ElementType(T : T[]) = T는 사실 template ElementType(T : T[]) { alias ElementType = T; }의 짧은 문법인 거예요.
중첩 템플릿 (Nested Templates)
템플릿이 집계 타입이나 함수의 지역 스코프에 선언되면, 인스턴스화된 함수들은 감싸고 있는 스코프의 컨텍스트(context)를 암묵적으로 포착(capture)해요.
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는 클래스 C 객체의 this를 포착하므로 멤버 함수처럼 동작하고(final 취급), Bar!().bar는 main()의 지역 변수 c를 포착하는 중첩 함수가 돼요.
집계 타입의 제한 (Aggregate Type Limitations)
중첩 템플릿은 집계 타입에 비정적(non-static) 필드를 추가할 수 없어요. 중첩 템플릿에 선언된 필드는 암묵적으로 static이 됩니다.
또 중첩 템플릿은 클래스나 인터페이스에 가상 함수를 추가할 수 없어요. 중첩 템플릿 안의 메서드는 암묵적으로 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
}
T xx는 Foo의 정적 필드가 되고, func는 final이 돼요. abstract는 final과 모순이라 에러죠. 또 bar.func(1)처럼 객체 없이 호출하면 this가 없어서 에러고, 반드시 o.TBar!int.func(1)처럼 객체를 통해 호출해야 합니다.
암묵적 중첩 (Implicit Nesting)
템플릿이 템플릿 별칭 매개변수를 가지면, 그 별칭이 가리키는 심볼이 있는 컨텍스트를 암묵적으로 포착할 수 있어요.
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
}
Foo!(C.num)은 멤버 num을 포착해서 fooX가 암묵적 멤버 함수가 되고, Foo!num은 지역 변수를 포착해서 중첩 함수가 돼요. 둘 다 &fooX가 delegate 객체가 된다는 점이 공통적입니다.
함수뿐 아니라, 인스턴스화된 클래스와 구조체 타입도 암묵적으로 포착된 컨텍스트를 통해 중첩될 수 있어요.
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);
}
인스턴스화된 클래스 N!int는 C 안에 중첩될 수 있어서 num에 접근해요. (Nested Class Instantiation도 함께 보세요.)
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);
}
구조체 템플릿도 마찬가지로 함수 main() 안에 중첩되어 지역 변수에 접근할 수 있어요.
템플릿화된 struct는 특정 값을 캡처하려고 할 때 주의가 필요해요. 예를 들어 아래처럼 별칭 매개변수 F가 지역 중첩 함수를 가리키는 경우죠.
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);
}
여기서 A!fun은 지역 중첩 함수 fun을 포착해서, 그 안의 x에 접근할 수 있어요.
컨텍스트 제한 (Context Limitation)
현재 중첩 템플릿은 최대 하나의 컨텍스트만 포착할 수 있어요. 전형적인 예로, 비정적 템플릿 멤버 함수는 템플릿 별칭 매개변수로 지역 심볼을 가져올 수 없습니다.
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()'
}
c.foo!var()는 this(클래스 C의 num)와 main()(지역 변수 var) 두 가지 컨텍스트를 동시에 요구하므로 실패해요.
하지만 한 컨텍스트가 다른 컨텍스트에서 간접적으로 접근 가능하면 허용됩니다.
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();
}
두 지역 변수 a와 b는 둘 다 main()의 nested() 컨텍스트 안에 있으므로, sum!(a, b)는 하나의 컨텍스트(nested())로 충분해서 동작해요.
재귀 템플릿 (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);
factorial!(4)는 컴파일 타임에 4 * 3 * 2 * 1 = 24로 평가돼요. 더 자세한 내용과 CTFE에 관한 내용은 Template Recursion 문서를 참고하세요.
템플릿 제약 (Template Constraints)
Constraint:
if ( Expression )
Constraint는 템플릿 정의의 TemplateParameterList 바로 뒤에 오는 if (표현식)이며, 그 Expression은 static if처럼 컴파일 타임에 평가되고, 참일 때만 매치가 성립합니다.
예를 들어 다음 함수 템플릿은 N이 홀수인 값에만 매치돼요.
void foo(int N)()
if (N & 1)
{
...
}
...
foo!(3)(); // OK, matches
foo!(4)(); // Error, no match
N & 1이 참인 N=3은 매치되고, 짝수인 N=4는 매치되지 않아요.
템플릿 제약은 집계 타입(구조체, 클래스, 유니온)에도 사용할 수 있어요. 제약은 라이브러리 모듈 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
if (isIntegral!T) 제약 때문에 int는 통과하지만 double은 제약을 만족하지 못해 에러가 나요. 이렇게 제약 조건으로 매치되는 타입을 세밀하게 통제할 수 있어요.
더 알아보기 (Learn more)
- 공식 D 언어 사양 — Templates 전체: https://dlang.org/spec/template.html
- D 언어 사양 홈
- 관련 챕터: Template Mixins, Conditional Compilation(version), Expressions
- 컴파일 타임 인자(CTFE): D Templates Revisited, Compile-time sequences
std.meta·std.traits·std.functional·std.typecons