テンプレート
テンプレートは、コードをパターンとして記述し、コンパイラが自動的にプログラムコードを生成できるようにする機能だ。ソースコードの一部は、プログラム内で実際に使用されるまでコンパイラに埋め込ませるようにすることができる。
テンプレートは、特定の型に縛られることなく、汎用的なアルゴリズムやデータ構造を記述できることから、ライブラリなどで特に有用だ。
他の言語のテンプレートサポートと比較すると、Dのテンプレートは非常に強力で広範だ。この章では、テンプレートの詳細については説明しない。関数、構造体、およびクラスのテンプレート、および型テンプレートパラメータについてのみ説明する。テンプレートの詳細については、テンプレートの詳細の章で説明する。Dテンプレートの完全なリファレンスについては、Philippe Sigaud 著のD Templates: A Tutorialを参照。
テンプレートの利点を見るために、括弧内の値を出力する関数から始めよう。
パラメータはintとして指定されているため、この関数はint型の値、またはintに自動的に変換できる値でのみ使用できる。例えば、コンパイラは浮動小数点型でこの関数を呼び出すことを許可しない。
プログラムの要件が変更され、他の型も括弧で囲んで出力する必要が生じたとしよう。この問題の解決策のひとつは、関数のオーバーロードを利用して、その関数が使用されるすべての型に対してオーバーロードを用意することだ。
この解決策は、realやユーザー定義の型などでは関数を使用できなくなるため、拡張性に欠ける。他の型に対して関数をオーバーロードすることは可能だが、そのコストは膨大になる可能性がある。
ここで重要な点は、パラメータの型に関係なく、オーバーロードの内容はすべて、単一のwritefln()式という同じものになることだ。
このような汎用性は、アルゴリズムやデータ構造ではよく見られる。例えば、二分探索アルゴリズムは、要素の型に依存しない。このアルゴリズムは、検索の具体的な手順と操作に関するものだからだ。同様に、リンクリストデータ構造も、要素の型に依存しない。リンクリストは、要素の型に関係なく、要素がコンテナにどのように格納されるかに関するものだからだ。
テンプレートはこのような状況で有用だ:コードがテンプレートとして記述されると、コンパイラはプログラム内のそのコードの実際の使用方法に応じて、同じコードのオーバーロードを自動的に生成する。
前述のように、この章では、関数、構造体、およびクラスのテンプレート、および型テンプレートパラメータについてのみ説明する。
関数テンプレート
関数をテンプレートとして定義すると、その関数で使用される1つ以上の型が未指定のままになり、後でコンパイラによって推論される。
指定されていない型は、関数名と関数パラメータリストの間に位置するテンプレートパラメータリスト内で定義される。そのため、関数テンプレートには2つのパラメータリスト、つまりテンプレートパラメータリストと関数パラメータリストがある。
上記のテンプレートパラメータリスト内のTは、Tが任意の型であることができることを意味する。Tは任意の名前だが、"型"の頭文字をとったもので、テンプレートではよく使われる。
Tは任意の型を表すため、上記のprintInParens()のテンプレート定義は、ユーザー定義のものを含め、ほぼすべての型で使用するのに十分だ。
コンパイラは、プログラム内のprintInParens()のすべての使用を考慮し、それらの使用をすべてサポートするコードを生成する。その後、この関数は、int、double、およびMyStructに対して明示的にオーバーロードされたかのようにコンパイルされる。
/* Note: These functions are not part of the source * code. They are the equivalents of the functions that * the compiler would automatically generate. */ void printInParens(int value) { writefln("(%s)", value); } void printInParens(double value) { writefln("(%s)", value); } void printInParens(MyStruct value) { writefln("(%s)", value); }
プログラムの出力は、関数テンプレートのこれらの異なるインスタンス化によって生成される。
(42)
(1.2)
(こんにちは)
各テンプレートパラメータは、複数の関数パラメータを決定することができる。例えば、次の関数テンプレートの2つの関数パラメータと戻り値の型は、その単一のテンプレートパラメータによって決定される。
複数のテンプレートパラメータ
関数テンプレートを変更して、括弧も受け取るようにしよう。
これで、異なる括弧のセットで同じ関数を呼び出すことができるようになった。
括弧を指定できることで関数の使いやすさは向上するが、括弧の型をcharと指定すると、wcharやdcharの型の文字で関数を呼び出せなくなるため、柔軟性が低下する。
1つの解決策は、括弧の型をdcharと指定することだが、この場合、stringやユーザー定義の型で関数を呼び出すことができなくなるため、まだ不十分だ。
別の解決策は、括弧の型もコンパイラに任せることだ。特定のcharの代わりに、追加のテンプレートパラメータを定義すれば十分だ。
新しいテンプレートパラメータの意味は、Tの意味と同様だ。ParensTypeは、任意の型にすることができる。
これで、さまざまな種類の括弧を使用することが可能になった。以下は、wcharおよびstringを使用した例だ。
→42←
-=1.2=-
printInParens()の柔軟性が向上し、TとParensTypeの組み合わせが、writeln()で表示可能であれば、正しく機能するようになった。
型の推論
テンプレートパラメータに使用する型をコンパイラが決定することを、型推論と呼ぶ。
上記の最後の例を続けて、コンパイラは関数テンプレートの2つの使用に応じて、次の型を決定する。
intwchar(42が表示された場合)doublestring(1.2が表示される場合)
コンパイラは、関数テンプレートに渡されるパラメータ値の型からのみ型を推論することができる。通常、コンパイラは型を曖昧さなく推論できるが、プログラマが型を明示的に指定しなければならない場合もある。
明示的な型指定
コンパイラがテンプレートパラメータを推測できない場合がある。この状況は、型が関数パラメータリストに現れない場合に発生する。テンプレートパラメータが関数パラメータに関連していない場合、コンパイラはテンプレートパラメータの型を推測できない。
この例を見るために、ユーザーに質問をし、その回答として値を読み取り、その値を返す関数を設計しよう。さらに、この関数を、あらゆる型の回答を読み取ることができるように、関数テンプレートにしよう。
この関数テンプレートは、入力からさまざまな型の値を読み込むプログラムで非常に便利だ。例えば、ユーザー情報を読み込む場合、次の行のように呼び出すことを想像できる。
残念ながら、この呼び出しでは、テンプレートパラメータTが何を指しているかをコンパイラに伝えることができない。質問がstringとして関数に渡されることはわかっているが、戻り値の型は推測できない。
このような場合、テンプレートパラメータはプログラマーが明示的に指定する必要がある。テンプレートパラメータは、感嘆符の後に括弧で囲んで指定する:
これで、上記のコードはコンパイラで受け入れられ、関数テンプレートは、テンプレートの定義内でTがintの別名として、次のようにコンパイルされる。
テンプレートパラメータが1つだけ指定されている場合、その周囲の括弧は省略可能だ:
この構文は、これまでのプログラムで使用してきたto!stringと似ている。to()は、変換のターゲット型をテンプレートパラメータとして取る関数テンプレートだ。指定する必要のあるテンプレートパラメータは1つだけなので、通常、to!(string)ではなくto!stringと記述する。
テンプレートのインスタンス化
特定のテンプレートパラメータ値のセットに対するコードの自動生成は、その特定のパラメータ値のセットに対するそのテンプレートのインスタンス化と呼ばれる。例えば、to!stringとto!intは、関数テンプレートtoの2つの異なるインスタンス化である。
後で別のセクションで再び述べるように、テンプレートの異なるインスタンス化は、異なる互換性のない型を生成する。
テンプレートの特殊化
getResponse()関数テンプレートは、理論的にはあらゆるテンプレート型に使用できるが、コンパイラが生成するコードは、すべての型に適しているとは限らない。2次元空間上の点を表す次の型があるとする。
Point型に対してgetResponse()をインスタンス化することは問題ないが、Pointに対して生成されたreadf()呼び出しはコンパイルできない。これは、標準ライブラリ関数readf()がPointオブジェクトの読み取り方法を知らないためだ。実際にレスポンスを読み込む2行は、関数テンプレートgetResponse()のインスタンス化Pointでは、次のように表示される。
Pointオブジェクトを読み込む1つの方法は、xおよびyメンバーの値を個別に読み込み、その値からPointオブジェクトを構築することだ。
特定のテンプレートパラメータ値に対するテンプレートの特別な定義を提供することを、テンプレートの特殊化と呼ぶ。特殊化は、テンプレートパラメータリストの:文字の後に続く型名によって定義される。関数テンプレートgetResponse()のPoint特殊化は、次のコードのように定義できる。
特化は、getResponse()の一般的な定義を利用して、xおよびyメンバーの値として使用する2つのint値を読み込むことに注意。
テンプレート自体をインスタンス化する代わりに、Point型に対してgetResponse()が呼び出されるたびに、コンパイラは上記の特殊化を使用する。
ユーザーが11と22を入力した場合:
中心はどこ? (Point)
x (int): 11
y (int): 22
getResponse!int()の呼び出しはテンプレートの一般定義に、getResponse!Point()の呼び出しはPointの特殊化にそれぞれ転送される。
別の例として、stringで同じテンプレートを使用することを考えてみよう。文字列の章で覚えているように、readf()は、入力の最後まで、入力からすべての文字を単一のstringの一部として読み込む。そのため、stringの応答を読み込む場合、getResponse()のデフォルトの定義は役に立たない。
stringに対してテンプレート特化を提供することもできる。次の特化は行のみを読み込む:
構造体およびクラステンプレート
Point構造体には、2つのメンバーがintとして明確に定義されているため、小数部の座標値を表現できないという制限がある。この制限は、Point構造体をテンプレートとして定義することで取り除くことができる。
まず、別のPointオブジェクトまでの距離を返すメンバー関数を追加しよう。
このPointの定義は、必要な精度が比較的低い場合に適している:例えば、組織の本社と支社間の距離をキロメートル単位で計算できる:
残念ながら、Pointは、intが提供できる精度を超える精度では不十分だ。
構造体およびクラスは、その名の後にテンプレートパラメータリストを指定することで、テンプレートとして定義することもできる。例えば、Pointは、テンプレートパラメータを指定し、intをそのパラメータに置き換えることで、構造体テンプレートとして定義することができる。
構造体およびクラスは関数ではないため、パラメータを指定して呼び出すことはできない。そのため、コンパイラはテンプレートパラメータを推測することができない。構造体およびクラステンプレートのテンプレートパラメータリストは、常に指定する必要がある。
上記の定義により、コンパイラは、Pointテンプレートのintインスタンス化のためのコードを生成する。これは、以前のテンプレートではない定義と同等だ。しかし、これで、あらゆる型で使用できるようになった。例えば、より精度が必要な場合は、doubleを使用する。
テンプレート自体は特定の型とは無関係に定義されているが、その単一の定義により、さまざまな精度の点を表現することが可能になる。
Pointをテンプレートに変換するだけで、テンプレートではない定義に従ってすでに記述されているコードでコンパイルエラーが発生する。例えば、getResponse()のPoint特殊化は、次のようにコンパイルできなくなった。
コンパイルエラーが発生する理由は、Point自体がもはや型ではないためだ。Pointは、構造体テンプレートになった。そのテンプレートのインスタンス化のみが型として扱われる。Pointのインスタンス化に対してgetResponse()を正しく特殊化するには、次の変更が必要だ。
- このテンプレート特殊化が
Pointのすべてのインスタンスをサポートするためには、テンプレートパラメータリストにPoint!Tを明示的に含める必要がある。これは、getResponse()の特殊化がPoint!Tに対して定義され、Tの値に関わらず適用されることを意味する。この特殊化はPoint!int、Point!doubleなどに一致する。 - 同様に、正しい型を応答として返すには、戻り値の型も
Point!Tとして指定する必要がある。 Point!Tのメンバーであるxおよびyの型は、intではなくTになったため、これらのメンバーはgetResponse!int()ではなくgetResponse!T()を呼び出して読み込む必要がある。getResponse!int()はPoint!intに対してのみ正しいからだ。- 1と2と同様に、戻り値の型は
Point!Tになる。 Point!int、Point!doubleなど、すべての型について型の名前を正確に表示するには、T.stringofを使う。
デフォルトのテンプレートパラメーター
テンプレートを使用するたびにテンプレートパラメータの型を指定するのは面倒な場合がある。特に、その型がほとんどの場合特定の型である場合はなおさらだ。例えば、getResponse()は、プログラムではほとんどの場合int型に対して呼び出され、double型に対して呼び出されるのはごくわずかな場所だけだ。
テンプレートパラメータのデフォルト型を指定することができ、型が明示的に指定されていない場合はその型が使用される。デフォルトのパラメータ型は、=文字の後に指定する。
上記のgetResponse()の呼び出しでは型が指定されていないため、Tはデフォルトの型intとなり、呼び出しはgetResponse!int()と同等になる。
デフォルトのテンプレートパラメータは、構造体およびクラスのテンプレートにも指定できるが、その場合は、テンプレートパラメータリストが空であっても、必ず記述する必要がある。
可変引数の章で見たデフォルトの関数パラメータ値と同様に、デフォルトのテンプレートパラメータは、すべてのテンプレートパラメータに対して、または最後のテンプレートパラメータに対して指定することができる。
この関数の最後の2つのテンプレートパラメータは指定しなくてもかまわないが、最初のパラメータは指定する必要がある。
この使用例では、2番目と3番目のパラメーターはそれぞれintとcharになる。
すべてのテンプレートのインスタンス化は、異なる型を生成する
特定の型セットに対するテンプレートのすべてのインスタンス化は、別個の型と見なされる。例えば、Point!intとPoint!doubleは別個の型だ。
これらの異なる型は、上記の代入演算では使用できない。
コンパイル時機能
テンプレートは完全にコンパイル時機能である。テンプレートのインスタンスは、コンパイラによってコンパイル時に生成される。
クラステンプレートの例: スタックデータ構造
構造体およびクラステンプレートは、データ構造の実装でよく使われる。任意の型を格納できるスタックコンテナを設計しよう。
スタックは最もシンプルなデータ構造の一つだ。これは、紙の束のように要素が概念的に積み重ねられたコンテナを表する。新しい要素は上に追加され、アクセスできるのは最も上の要素のみだ。要素が削除されると、必ず最も上の要素が削除される。
スタック内の要素の総数を返すプロパティも定義すると、このデータ構造のすべての操作は次のようになる。
- 要素を追加(
push()) - 要素の削除(
pop()) - 最上部の要素にアクセスする(
.top) - 要素の数を報告する(
.length)
配列を使用して、配列の最後の要素がスタックの最上位の要素を表すように要素を格納することができる。最後に、任意の型の要素を含むことができるように、クラステンプレートとして定義することができる。
このクラス用のunittestブロック(intインスタンス化を使用)は以下の通りだ:
このクラステンプレートを利用するために、今回はユーザー定義型で試しよう。例として、Pointを変更したバージョンを以下に示する。
Point!double型の要素を含むStackは、次のように定義できる。
このスタックに10個の要素を追加し、その後1つずつ削除するテストプログラムは以下の通りだ:
プログラムの出力からわかるように、要素は追加された順序と逆の順序で削除されている:
| 操作 | Point.x | Point.y |
|---|---|---|
| 加算 | -0.02 | -0.01 |
| 加算 | 0.17 | -0.5 |
| 加算 | 0.12 | 0.23 |
| 加算 | -0.05 | -0.47 |
| 加算 | -0.19 | -0.11 |
| 加算 | 0.42 | -0.32 |
| 加算 | 0.48 | -0.49 |
| 加算 | 0.35 | 0.38 |
| 加算 | -0.2 | -0.32 |
| 加算 | 0.34 | 0.27 |
| 削除 | 0.34 | 0.27 |
| 削除 | -0.2 | -0.32 |
| 削除 | 0.35 | 0.38 |
| 削除 | 0.48 | -0.49 |
| 削除 | 0.42 | -0.32 |
| 削除 | -0.19 | -0.11 |
| 削除 | -0.05 | -0.47 |
| 削除 | 0.12 | 0.23 |
| 削除 | 0.17 | -0.5 |
| 削除 | -0.02 | -0.01 |
関数テンプレートの例: 二分探索アルゴリズム
二分探索は、既にソートされた配列内の要素を検索する最も高速なアルゴリズムだ。非常にシンプルなアルゴリズムである:中央の要素を検討する。その要素が検索対象の要素であれば、検索は終了する。そうでない場合、検索対象の要素が中央の要素よりも大きい場合は左側の要素、小さい場合は右側の要素に対してアルゴリズムを繰り返し実行する。
初期要素のより小さな範囲で自分自身を繰り返すアルゴリズムは再帰的だ。自身を呼び出すことで、二分探索アルゴリズムを再帰的に記述しよう。
テンプレートに変換する前に、まずintの配列のみをサポートするこの関数を記述しよう。後でテンプレートパラメータリストを追加し、定義内の適切なintをTに置き換えることで、簡単にテンプレートに変換することができる。以下は、intの配列で動作する二分探索アルゴリズムだ。
上記の関数は、この単純なアルゴリズムを4つのステップで実装している。
- 配列が空の場合は、
size_t.maxを返して、値が見つからないことを示す。 - 中央の要素が検索値と等しい場合は、その要素のインデックスを返する。
- 値が中央の要素よりも小さい場合は、左側で同じアルゴリズムを繰り返す。
- それ以外の場合、右側で同じアルゴリズムを繰り返す。
この関数をテストするunittestブロックは次の通りだ。
intの関数が実装され、テストされたので、これをテンプレートに変換することができる。intは、テンプレートの定義の中で2箇所だけに登場する。
パラメータリストに現れるintは、要素と値の型である。これらをテンプレートパラメータとして指定することで、このアルゴリズムをテンプレート化し、他の型でも使用可能になる。
この関数テンプレートは、テンプレートでその型に適用される操作と一致するあらゆる型で使用できる。binarySearch()では、要素は比較演算子==および<でのみ使用される。
そのため、PointはまだbinarySearch()と一緒に使用することはできない:
上記のプログラムはコンパイルエラーを引き起こす:
エラーメッセージによると、opCmp()はPointに対して定義する必要がある。opCmp()は、演算子オーバーロードの章で説明した。
まとめ
テンプレートの他の機能については後続の章で説明する。この章で扱った内容は以下の通りだ:
- テンプレートは、プログラム内の実際の使用状況に応じてコンパイラがインスタンスを生成するためのパターンとしてコードを定義する。
- テンプレートはコンパイル時機能である。
- テンプレートパラメータのリストを指定するだけで、関数、構造体、およびクラスの定義をテンプレートにすることができる。
- テンプレート引数は、感嘆符の後に明示的に指定できる。括弧内に1つのトークンのみがある場合は、括弧は不要だ。
- すべてのテンプレートのインスタンス化は、固有の型を生成する。
- テンプレート引数は、関数テンプレートに対してのみ推測できる。
- テンプレートは、
:文字の後の型に対して特化することができる。 - デフォルトのテンプレート引数は、
=文字の後に指定する。