テンプレート
I think that I can safely say that nobody understands template mechanics. -- Richard Deyman
テンプレートは、D でジェネリックプログラミングを行う方法です。 TemplateDeclaration 宣言で定義します:
TemplateDeclaration:
template TemplateIdentifier ( TemplateParameterList ) Constraintopt
{ DeclDefs }
TemplateIdentifier:
Identifier
TemplateParameterList:
TemplateParameter
TemplateParameter ,
TemplateParameter , TemplateParameterList
TemplateParameter:
TemplateTypeParameter
TemplateValueParameter
TemplateAliasParameter
TemplateTupleParameter
TemplateThisParameter
TemplateDeclaration のボディ部は、 例えインスタンス化されることがなくても 文法的に正しいものである必要があります。意味解析はインスタンス化されるまでは 行われません。テンプレートは独自のスコープを成し、クラス、構造体、型定義、 列挙型、変数、関数、他のテンプレート、を含むことが出来ます。
テンプレートの引数は型,値,シンボル,もしくはタプルです。 型はどんな型でも構いません。 値パラメタは整数型、 浮動小数点型、文字列型のみで、 その特殊化版には整数定数、浮動小数定数、null、 もしくは文字列リテラルへ評価される式が必要です。 シンボルには任意の非ローカルシンボルを利用できます。 タプルは、型,値,シンボルの0個以上の組です。
テンプレート引数の特殊化は、 TemplateParameter が受け取れる値や型を制限します。
テンプレート引数のデフォルトは、値や型が TemplateParameter に指定されなかったときに使用されます。
明示的なテンプレートのインスタンス化
テンプレートを明示的にインスタンス化する構文は:
TemplateInstance:
TemplateIdentifier !( TemplateArgumentList )
TemplateIdentifier ! TemplateSingleArgument
TemplateArgumentList:
TemplateArgument
TemplateArgument ,
TemplateArgument , TemplateArgumentList
TemplateArgument:
Type
AssignExpression
Symbol
Symbol:
SymbolTail
. SymbolTail
SymbolTail:
Identifier
Identifier . SymbolTail
TemplateInstance
TemplateInstance . SymbolTail
TemplateSingleArgument:
Identifier
BasicTypeX
CharacterLiteral
StringLiteral
IntegerLiteral
FloatLiteral
true
false
null
__FILE__
__LINE__
一度インスタンス化されると、 テンプレート内の宣言(テンプレートメンバ)は、 TemplateInstance のスコープへ入ります。
template TFoo(T) { alias T* t; }
...
TFoo!(int).t x; // x を型 int* として宣言
TemplateArgument の引数が1トークンなら、括弧を省略できます:
TFoo!int.t x; // TFoo!(int).t x; と同じ
テンプレートのインスタンス化はaliasで別名を付けることができます:
template TFoo(T) { alias T* t; }
alias TFoo!(int) abc;
abc.t x; // x を型 int* として宣言
暗黙変換前の値が同じ TemplateArgumentList で複数回 TemplateDeclaration をインスタンス化すると、同じインスタンスが参照されます。 例えば:
template TFoo(T) { T f; }
alias TFoo!(int) a;
alias TFoo!(int) b;
...
a.f = 3;
assert(b.f == 3); // a と b は TFoo の同じインスタンスを指す
これは、 テンプレートのインスタンス化が異なるモジュールで為された場合も成り立ちます。
なお、テンプレート引数が暗黙変換の効果で同じテンプレートパラメータ型に 結果として落ち着くケースは、別々のインスタンスとなります:
struct TFoo(int x) { }
static assert(is(TFoo!(3) == TFoo!(2 + 1))); // 3 と 2+1 はどちらもint型の 3
static assert(!is(TFoo!(3) == TFoo!(3u))); // 3u と 3 は異なる型
複数のテンプレートが同じ TemplateIdentifier で宣言された場合、 パラメタの数が違うか、異なる特殊化がなされていれば、 違うテンプレートとして区別されます。
テンプレートの例として、ジェネリックなコピー関数があります:
template TCopy(T) {
void copy(out T to, T from) {
to = from;
}
}
このテンプレートを使うには、 まず特定の型でインスタンス化します:
int i;
TCopy!(int).copy(i, 3);
インスタンス化のスコープ
TemplateInstantance は TemplateDeclaration の宣言されたスコープに あるものとして常に扱われます。テンプレート仮引数は、 パラメタから推論された型へのaliasとしてスコープに追加されます。
例えば:
module a
template TFoo(T) { void bar() { func(); } }
module b
import a;
void func() { }
alias TFoo!(int) f; // エラー: func はモジュール a で定義されていない
そして:
module a
template TFoo(T) { void bar() { func(1); } }
void func(double d) { }
module b
import a;
void func(int i) { }
alias TFoo!(int) f;
...
f.bar(); // a.func(double) を呼び出す
TemplateParameter の特殊化とデフォルト値は、 TemplateDeclaration のスコープ内で評価されます。
引数推論
テンプレートパラメタの型は、 インスタンス化時に、 対応する引数と比較することで推論されます。
各々の型パラメータについて、全てのパラメータの型が推論されるまで 以下の規則が適用されます:
- そのパラメタに対する特殊化版が存在しなければ、 パラメタ型はテンプレート引数そのものに設定されます。
- 型パラメタに依存した特殊化版があれば、 そのパラメタ内の型は、 テンプレート引数型の対応する部分へと設定されます。
- 全ての型引数を調べた後、 まだ型の決定されていないパラメタがあれば、 そのパラメタは TemplateArgumentList 中で同じ位置にある引数型へと設定されます。
- 以上のルールで一意に定まらないパラメタがあれば、 エラーです。
例えば:
template TFoo(T) { }
alias TFoo!(int) Foo1; // (1) T は int と推論される
alias TFoo!(char*) Foo2; // (1) T は char* と推論される
template TBar(T : T*) { }
alias TBar!(char*) Foo3; // (2) T は char と推論される
template TAbc(D, U : D[]) { }
alias TAbc!(int, int[]) Bar1; // (2) D は int と推論される。U は int[]
alias TAbc!(char, int[]) Bar2; // (4) エラー。D が char でも int でもある
template TDef(D : E*, E) { }
alias TDef!(int*, int) Bar3; // (1) E は int
// (3) D は int*
特殊化時の推論では、 二つ以上の型パラメータを決定することも可能です:
template Foo(T: T[U], U) {
...
}
Foo!(int[long]) // Fooを、TをintにUをlongにしてインスタンス化
マッチを取る際には、 クラスはその基底クラスやインターフェイスにもマッチします:
class A { }
class B : A { }
template TFoo(T : A) { }
alias TFoo!(B) Foo4; // (3) T は B
template TBar(T : U*, U : A) { }
alias TBar!(B*, B) Foo5; // (2) T は B*
// (3) U は B
型パラメタ
TemplateTypeParameter:
Identifier
Identifier TemplateTypeParameterSpecialization
Identifier TemplateTypeParameterDefault
Identifier TemplateTypeParameterSpecialization TemplateTypeParameterDefault
TemplateTypeParameterSpecialization:
: Type
TemplateTypeParameterDefault:
= Type
特殊化
テンプレートは、 識別子 : 特殊型 という形式で、 特定の型に対して特殊化することができます。 例:
template TFoo(T) { ... } // #1
template TFoo(T : T[]) { ... } // #2
template TFoo(T : char) { ... } // #3
template TFoo(T,U,V) { ... } // #4
alias TFoo!(int) foo1; // #1 をインスタンス化
alias TFoo!(double[]) foo2; // #2 をインスタンス化。T は double
alias TFoo!(char) foo3; // #3 をインスタンス化
alias TFoo!(char, int) fooe; // エラー。引数の個数が合わない
alias TFoo!(char, int, int) foo4; // #4 をインスタンス化
選択されるテンプレートは、 TemplateArgumentList の型があって、 かつ最も特殊化度が高いものになります。 どれが一番特殊化されているかは、 C++ の Partial Ordering 規則と同様に決定します。あいまいな場合はエラーです。
thisパラメタ
TemplateThisParameter:
this TemplateTypeParameter
TemplateThisParameter はメンバ関数テンプレートで使われ、 this の型を得ることができます。
import std.stdio;
struct S {
const void foo(this T)(int i) {
writeln(typeid(T));
}
}
void main() {
const(S) s;
(&s).foo(1);
S s2;
s2.foo(2);
immutable(S) s3;
s3.foo(3);
}
出力は:
const(S)
S
immutable(S)
値パラメタ
TemplateValueParameter:
BasicType Declarator
BasicType Declarator TemplateValueParameterSpecialization
BasicType Declarator TemplateValueParameterDefault
BasicType Declarator TemplateValueParameterSpecialization TemplateValueParameterDefault
TemplateValueParameterSpecialization:
: ConditionalExpression
TemplateValueParameterDefault:
= __FILE__
= __LINE__
= AssignExpression
__FILE__ と __LINE__ は、 テンプレートがインスタンス化された場所のファイル名と行番号に展開されます。
テンプレートの値引数の型は、 コンパイル時に静的初期化可能な任意の型を指定可能です。 値引数には、整数、浮動小数点数、 null、文字列、テンプレート値引数の配列リテラル、 テンプレート値引数の連想配列リテラル、 テンプレート値引数の構造体リテラルが使えます。
template foo(string s) {
string bar() { return s ~ " betty"; }
}
void main() {
writefln("%s", foo!("hello").bar()); // 表示: hello betty
}
この例は、 値パラメタを10に特殊化したテンプレートfooの例です:
template foo(U : int, int T : 10) {
U x = T;
}
void main() {
assert(foo!(int, 10).x == 10);
}
aliasパラメタ
TemplateAliasParameter:
alias Identifier TemplateAliasParameterSpecializationopt TemplateAliasParameterDefaultopt
alias BasicType Declarator TemplateAliasParameterSpecializationopt TemplateAliasParameterDefaultopt
TemplateAliasParameterSpecialization:
: Type
: ConditionalExpression
TemplateAliasParameterDefault:
= Type
= ConditionalExpression
aliasパラメタによって、テンプレートの引数に様々な D のシンボルを受け取るようにできます。alias パラメタに使えるシンボルは グローバル/ローカル名とtypedef名、テンプレート名、テンプレートインスタンスの名前です。 リテラルをalias引数に渡すことも可能です。
- グローバル名
int x; template Foo(alias X) { static int* p = &X; } void test() { alias Foo!(x) bar; *bar.p = 3; // x を 3 に設定 static int y; alias Foo!(y) abc; *abc.p = 3; // y を 3 に設定 }
- 型名
class Foo { static int p; } template Bar(alias T) { alias T.p q; } void test() { alias Bar!(Foo) bar; bar.q = 3; // Foo.p を 3 に設定 }
- モジュール名
import std.string; template Foo(alias X) { alias X.toString y; } void test() { alias Foo!(std.string) bar; bar.y(3); // std.string.toString(3) を呼ぶ }
- テンプレート名
int x; template Foo(alias X) { static int* p = &X; } template Bar(alias T) { alias T!(x) abc; } void test() { alias Bar!(Foo) bar; *bar.abc.p = 3; // x を 3 に設定 }
- テンプレートの別名
int x; template Foo(alias X) { static int* p = &X; } template Bar(alias T) { alias T.p q; } void test() { alias Foo!(x) foo; alias Bar!(foo) bar; *bar.q = 3; // x を 3 に設定 }
- リテラル
template Foo(alias X, alias Y) { static int i = X; static string s = Y; } void test() { alias Foo!(3, "bar") foo; writeln(foo.i, foo.s); // 3bar と表示 }
タプルパラメタ
TemplateTupleParameter:
Identifier ...
テンプレートの最後の引数が TemplateParameterList として宣言されていた場合、 その引数はテンプレートに渡された引数の末尾の列とマッチし、 その列が Tuple となります。 Tuple は、型でも、式でも、シンボルでもありません。 型と式とシンボルの混ざった列が、Tuple です。
Tuple の要素が全て型であるようなとき、 TypeTuple と呼びます。 Tuple の要素が全て式であるようなときは、 ExpressionTuple と呼びます。
Tuple は、他のテンプレートをインスタンス化するのに使ったり、 関数の引数リストとして使用したりできます。
template Print(A ...) {
void print() {
writefln("args are ", A);
}
}
template Write(A ...) {
void write(A a) // A は TypeTuple
// a は ExpressionTuple
{
writefln("args are ", a);
}
}
void main() {
Print!(1,'a',6.8).print(); // args are 1a6.8 と表示
Write!(int, char, double).write(1, 'a', 6.8); // args are 1a6.8 と表示
}
タプル引数は、 関数テンプレートの暗黙のインスタンス化の際には、 末尾の引数の型から決定されます:
template Foo(T, R...) {
void Foo(T t, R r) {
writefln(t);
static if (r.length) // さらに引数があれば、
Foo(r); // 残りについても処理をする
}
}
void main() {
Foo(1, 'a', 6.8);
}
表示は以下のようになります:
1
a
6.8
タプルは、関数の引数として渡された delegate や関数の引数リストから導出することも可能です:
import std.stdio;
/* R は返値型
* A は先頭の引数型
* U は残りの引数の TypeTuple
*/
R delegate(U) Curry(R, A, U...)(R delegate(A, U) dg, A arg)
{
struct Foo
{
typeof(dg) dg_m;
typeof(arg) arg_m;
R bar(U u)
{
return dg_m(arg_m, u);
}
}
Foo* f = new Foo;
f.dg_m = dg;
f.arg_m = arg;
return &f.bar;
}
void main()
{
int plus(int x, int y, int z)
{
return x + y + z;
}
auto plus_two = Curry(&plus, 2);
writefln("%d", plus_two(6, 8)); // 16 と表示
}
Tuple の要素数は、 .length プロパティで取得可能です。第 n番目の要素は 添え字アクセス [n] で取得できますし、 部分タプルはスライスによって取り出すことができます。
Tuple は静的な、コンパイル時限定の要素です。 動的に要素の変更・追加・削除を行うことはできません。
TemplateTupleParameter なしのテンプレートと有りのテンプレートがどちらもインスタンス化にマッチする場合は、 ない方が優先されます。
テンプレート引数のデフォルト値
末尾のテンプレート引数にはデフォルト値を指定できます:
template Foo(T, U = int) { ... }
Foo!(uint,long); // Fooを、Tをuint, Uをlongとしてインスタンス化
Foo!(uint); // Fooを、Tをuint、Uをintとしてインスタンス化
template Foo(T, U = T*) { ... }
Foo!(uint); // Fooを、Tをuint, Uをuint*としてインスタンス化
テンプレートの暗黙プロパティ
もしテンプレートが丁度ひとつだけメンバを持ち、 しかもその名前がテンプレートの名前と同じであれば、 そのメンバはテンプレートのインスタンス化によって参照されることになります:
template Foo(T) {
T Foo; // 型Tの変数Fooを宣言
}
void test() {
Foo!(int) = 6; // Foo!(int).Foo の代わりに
}
コンストラクタテンプレート
TemplatedConstructor:
this ( TemplateParameterList ) Parameters Constraintopt FunctionBody
テンプレートはクラスや構造体のコンストラクタを作るのにも使用できます。
クラステンプレート
ClassTemplateDeclaration:
class Identifier ( TemplateParameterList ) Constraintopt BaseClassListopt ClassBody
もしテンプレートが丁度ひとつのメンバを持ち、 そのメンバがテンプレートと同じ名前のクラスであれば:
template Bar(T) {
class Bar {
T member;
}
}
その時は、意味的に同等な ClassTemplateDeclaration で書くことができます:
class Bar(T) {
T member;
}
構造体、共用体、インターフェイステンプレート
StructTemplateDeclaration:
struct Identifier ( TemplateParameterList ) Constraintopt StructBody
UnionTemplateDeclaration:
union Identifier ( TemplateParameterList ) Constraintopt StructBody
InterfaceTemplateDeclaration:
interface Identifier ( TemplateParameterList ) Constraintopt BaseInterfaceListopt InterfaceBody
クラステンプレートの場合と同様に、構造体、共用体、 インターフェイスもテンプレート引数リストを加えることでテンプレート化することが可能です。
関数テンプレート
テンプレートが一つしかメンバを持たず、 そのメンバがテンプレートと同じ名前を持つ関数だった場合、関数テンプレート宣言となります。 別の手段として、実行時 パラメタ の直前に TemplateParameterList を並べて関数宣言することでも、関数テンプレートを宣言できます。
T 型の二乗を計算する関数は:
T Square(T)(T t) {
return t * t;
}
関数テンプレートは、 !(TemplateArgumentList) で明示的に特殊化することも可能ですし:
writefln("The square of %s is %s", 3, Square!(int)(3));
暗黙に、実引数の型から TemplateArgumentList を推論させることも可能です:
writefln("The square of %s is %s", 3, Square(3)); // T は int と推論される
TemplateArgumentList として渡された実引数の個数が TemplateParameterList の仮引数の個数より少ない場合は、 仮引数が左から順に埋められて行き、 埋まらなかった部分が実引数の型から暗黙推論されます。
暗黙推論したい関数テンプレート引数は、 特殊化することはできません:
void Foo(T : T*)(T t) { ... }
int x,y;
Foo!(int*)(x); // ok, Tは関数引数から推論されているのではない
Foo(&y); // エラー。T は特殊化されている
暗黙推論されるもの以外のテンプレート引数にはデフォルト値を指定できます:
void Foo(T, U=T*)(T t) { U p; ... }
int x;
Foo(&x); // T は int, U は int*
関数テンプレートの返値型は、 関数内の最初の ReturnStatement の結果から自動的に推論させることができます:
auto Square(T)(T t) {
return t * t;
}
複数のreturn文がある場合は、 全てのreturn文の式の型が正しく適合している必要があります。 return文が存在しない場合は、 返値型は void となります。
auto ref 引数関数テンプレート
関数テンプレートの引数が auto ref と宣言された場合、 lvalue が渡された時は ref となり、 そうでないときは値引数となります。
int foo(T...)(auto ref T x) {
int result;
foreach (i, v; x)
{
if (v == 10)
assert(__traits(isRef, x[i]));
else
assert(!__traits(isRef, x[i]));
result += v;
}
return result;
}
void main() {
int y = 10;
int r;
r = foo(8); // 8 を返す
r = foo(y); // 10 を返す
r = foo(3, 4, y); // 17 を返す
r = foo(4, 5, y); // 19 を返す
r = foo(y, 6, y); // 26 を返す
}
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 x = 7, y = 8;
int i;
i = min(4, 3); // 3 を返す
i = min(x, y); // 7 を返す
min(x, y) = 10; // x を 10 に設定
static assert(!__traits(compiles, min(3, y) = 10));
static assert(!__traits(compiles, min(y, 3) = 10));
}
再帰的テンプレート
テンプレート機能は組み合わせて使うことで、 単純でない関数のコンパイル時評価など、面白い効果を生むことができます。 例えば、階乗を計算するテンプレートはこう書かれます:
template factorial(int n : 1) {
enum { factorial = 1 }
}
template factorial(int n) {
enum { factorial = n* factorial!(n-1) }
}
void test() {
writefln("%s", factorial!(4)); // 24と表示
}
テンプレート制約
Constraint:
if ( ConstraintExpression )
ConstraintExpression:
Expression
Constraintは、 TemplateParameterList に追加して引数に制約を課すために使うことができます。 ConstraintExpression には、コンパイル時に計算でき、 bool 形に変換可能な式を指定します。 その値が true ならばテンプレートがマッチし、 そうでなければマッチしません。
例えば、以下の関数テンプレートは N が奇数の場合にのみマッチします:
void foo(int N)()
if (N & 1)
{
...
}
...
foo!(3)(); // ok、マッチ
foo!(4)(); // エラー、マッチしない
制限事項
テンプレートは、 クラスに非staticメンバや仮想関数を追加するのには使えません。 例えば:
class Foo {
template TBar(T) {
T xx; // Foo の static メンバとなる
int func(T) { ... } // non-virtual
static T yy; // Ok
static int func(T t, int y) { ... } // Ok
}
}
関数内ではテンプレートは宣言できません。
テンプレートで interface に関数を追加することはできません。
interface TestInterface { void tpl(T)(); } // エラー