その他のテンプレート
テンプレートの章で、テンプレートの威力と利便性について見てきた。アルゴリズムやデータ構造を1つのテンプレートで定義しておけば、その定義を複数の型に使用することができる。
その章では、テンプレートの最も一般的な使用法、つまり関数、struct
、class
テンプレート、およびそれらの型テンプレートパラメータでの使用法についてのみ説明した。この章では、テンプレートについてさらに詳しく説明する。先に進む前に、少なくともその章の要約セクションを確認しておくことをお勧めする。
ショートカット構文
Dのテンプレートは、強力であるだけでなく、定義や使用も簡単で、非常に読みやすい。関数、struct
、またはclass
テンプレートの定義は、テンプレートパラメータのリストを指定するだけで簡単だ。
上記のテンプレート定義は、Dのショートカットテンプレート構文を活用している。
完全な構文では、テンプレートはtemplate
キーワードで定義される。上記の2つのテンプレート定義の対応する完全な構文は次の通りだ:
ほとんどのテンプレートはショートカット構文で定義されるが、コンパイラは常に完全な構文を使用する。コンパイラがショートカット構文を完全な形式に変換する際に、次のようなステップを実行していると想像できる:
- 定義をテンプレートブロックで囲む。
- そのブロックに同じ名前を付ける。
- テンプレートパラメータリストをテンプレートブロックに移動する。
これらのステップを経て到達した完全な構文は、同名テンプレートと呼ばれ、プログラマーが明示的に定義することもできる。同名テンプレートについては後で説明する。
テンプレート名前空間
テンプレートブロック内には複数の定義を含めることができる。次のテンプレートには、関数とstruct
の定義の両方が含まれている。
特定の型に対してテンプレートをインスタンス化すると、ブロック内のすべての定義がインスタンス化される。次のコードは、int
およびdouble
に対してテンプレートをインスタンス化する。
テンプレートの特定のインスタンス化は名前空間を導入する。インスタンス化内の定義は、その名前で参照できる。ただし、これらの名前が長すぎる場合は、alias
の章で見たようにエイリアスを使用できる:
同名テンプレート
同名テンプレートは、そのブロックと同じ名前を持つ定義を含むtemplate
ブロックだ。実際、各ショートカットテンプレート構文は、同名テンプレートのショートカットだ。
例として、20バイトを超える型を大きすぎるとして修飾する必要があるプログラムがあるとする。このような修飾は、テンプレートブロック内の定数bool
値によって実現できる。
テンプレートブロックとその唯一の定義の名前が同じであることに注意。この同名のテンプレートは、isTooLarge!int.isTooLarge
全体の代わりにショートカット構文で使用される。
上記の強調表示部分は、ブロック内のbool
値と同じである。int
のサイズは 20 未満であるため、コードの出力はfalse
になる。
この同名テンプレートは、ショートカット構文でも定義できる:
同名のテンプレートは、特定の条件に応じて型エイリアスを定義する場合によく使われる。例えば、次の同名のテンプレートは、エイリアスを設定して2つの型のうち大きい方を選ぶ。
long
はint
よりも大きい(8バイト対4バイト)ため、LargerOf!(int, long)
は型long
と同じになる。このようなテンプレートは、2つの型自体がテンプレートパラメータである(またはテンプレートパラメータに依存している)他のテンプレートで特に役立つ。
テンプレートの種類
関数、クラス、および構造体テンプレート
関数、class
、struct
テンプレートについては、テンプレートの章で既に説明し、それ以降でも多くの例を見てきた。
メンバー関数テンプレート
struct
およびclass
のメンバー関数もテンプレートにすることができる。例えば、次のput()
メンバー関数テンプレートは、その型がテンプレート内の操作と互換性がある限り、あらゆるパラメータ型で機能する(この特定のテンプレートの場合、string
に変換可能である必要がある)。
ただし、テンプレートはインスタンス化回数が無限になる可能性があるため、コンパイラはインターフェースにどのテンプレートのインスタンスを含めるべきかを判断できないため、テンプレートは仮想関数にはできない。(したがって、abstract
キーワードも使用できない。)
例えば、次のサブクラスにput()
テンプレートが存在すると、関数がオーバーライドされているように見えるが、実際にはスーパークラスのput
名が隠されている (エイリアスの章の"名前の隠蔽"を参照)。
その結果、オブジェクトは実際にはSpecialSink
だが、fillSink()
内の両方の呼び出しはSink
にディスパッチされ、SpecialSink.put()
が挿入する波括弧はコンテンツに含まれない:
42hello ← SpecialSinkの動作ではなく、Sinkの動作
共用体テンプレート
共用体テンプレートは、構造体テンプレートと似ている。共用体テンプレートにも、ショートカット構文を使うことができる。
例として、共用体の章で見たIpAdress
union
のより一般的なバージョンを設計しよう。そこでは、IPv4アドレスの値は、以前のバージョンのIpAdress
のuint
メンバーとして保持され、セグメント配列の要素型はubyte
だった。
bytes
配列は、IPv4アドレスの4つのセグメントへの簡単なアクセスを提供していた。
同じ概念は、以下のunion
テンプレートのように、より一般的な方法で実装できる:
このテンプレートを使用すると、値とそのセグメントの型を自由に指定することができる。
必要なセグメントの数は、実際の値とセグメントの型によって異なる。IPv4アドレスには4つのubyte
セグメントがあるため、その値は、IpAddress
の以前の定義では4
とハードコードされていた。SegmentedValue
テンプレートでは、2つの特定の型に対してテンプレートがインスタンス化されるコンパイル時に、セグメントの数を計算する必要がある。
次の同名のテンプレートは、2つの型の.sizeof
プロパティを利用して、必要なセグメントの数を計算している。
ショートカット構文の方が読みやすいかもしれない:
注釈:式SegmentT.sizeof - 1
は、型のサイズが均等に分割できない場合に使用する。例えば、実際の型が5バイトで、セグメント型が2バイトの場合、合計3つのセグメントが必要だが、整数除算5/2の結果は2となり、不正確になる。
共用体テンプレートの定義はこれで完了。
uint
とubyte
のテンプレートのインスタンス化は、以前のIpAddress
の定義と等価である:
プログラムの出力は、共用体の章と同じだ。
2 | 1 | 168 | 192 |
このテンプレートの柔軟性を示すために、IPv4アドレスのパーツを2つのushort
値としてアクセスする必要があると想像しよう。セグメント型としてushort
を指定するだけで、簡単に実現できる。
IPv4 アドレスでは珍しいことだが、プログラムの出力は2つのushort
セグメント値で構成される。
258 | 49320 |
インターフェーステンプレート
インターフェーステンプレートは、インターフェースで使用される型(および固定長配列のサイズなどの値やインターフェースのその他の機能)に柔軟性を与える。
色のテンプレートパラメータによって色の型が決定される、色付きオブジェクト用のインターフェースを定義しよう。
このインターフェーステンプレートでは、そのサブタイプはpaint()
関数を定義する必要があるが、色の型は柔軟に指定できる。
Webページ上のフレームを表すクラスは、赤、緑、青の3つの成分で表される色型を使用することを選択できる。
一方、光の周波数を使用するクラスは、色を表すためにまったく別の型を選択することができる。
ただし、テンプレートの章で説明したように、"すべてのテンプレートのインスタンス化は、異なる型を生成する"ので、ColoredObject!RGB
とColoredObject!Frequency
は無関係のインターフェースであり、PageFrame
とBulb
は無関係のクラスになる。
テンプレートパラメータの種類
これまで見てきたテンプレートパラメータは、すべて型のパラメータだった。これまで、T
やColorT
などのパラメータは、すべて型を表していた。例えば、T
は、テンプレートのインスタンス化に応じて、int
、double
、Student
などを意味していた。
テンプレートパラメータには、value、this
、alias
、およびtupleという他の種類もある。
型のテンプレートパラメータ
このセクションは、完全を期すために記載している。これまで見てきたテンプレートはすべて、型のパラメータを持っていた。
値のテンプレートパラメータ
値のテンプレートパラメータを使用すると、テンプレート実装で使用される特定の値を柔軟に変更することができる。
テンプレートはコンパイル時の機能であるため、値のテンプレートパラメータの値はコンパイル時に既知でなければならない。実行時に計算しなければならない値は使用できない。
値のテンプレートパラメータの利点を理解するために、幾何学的形状を表す一連の構造体から始めよう。
これらの型の他のメンバー変数およびメンバー関数はまったく同じであり、唯一の違いは角の数を決定する値だけであると仮定しよう。
このような場合、値のテンプレートパラメータが役立つ。次の構造体テンプレートは、上記のすべての型などを表現するのに十分だ。
この構造体テンプレートの唯一のテンプレートパラメータは、size_t
型の値N
である。値N
は、テンプレート内のどこでもコンパイル時の定数として使用できる。
このテンプレートは、任意の辺の長さを持つ形状を表現するのに十分な柔軟性がある:
以下のエイリアスは、前述の構造体定義に対応している。
上記の値テンプレートパラメータの型はsize_t
だった。値がコンパイル時にわかる限り、値のテンプレートパラメータは、基本型、struct
型、配列、文字列など、あらゆる型にすることができる。
次の例では、string
テンプレートパラメータを使用してXMLタグを表し、単純なXML出力を作成している。
- まず、
<
>
の文字で囲まれたタグ:<tag>
- 次に、値
- 最後に、
</
>
の文字で囲まれたタグ:</tag>
例えば、位置 42を表すXMLタグは、<location>42</location>
と出力される。
テンプレートパラメータは、テンプレートの実装で使用される型ではなく、string
の値に関するものであることに注意。その値は、テンプレート内のどこでもstring
として使うことができる。
プログラムが必要とするXML要素は、次のコードのようにエイリアスとして定義できる。
出力:
[<location>1</location>, <temperature>23</temperature>, <weight>78</weight>]
値のテンプレートパラメータにもデフォルト値を設定することができる。例えば、次の構造体テンプレートは、デフォルトの次元数が3である多次元空間内の点を表している。
このテンプレートは、dimension
テンプレートパラメーターを指定せずに使用できる:
必要に応じて次元数を指定することもできる:
パラメータの数の可変性の章で、特別なキーワードは、コード内で使用される場合とデフォルトの関数引数として使用される場合で動作が異なることを説明した。
同様に、デフォルトのテンプレート引数として使用される特殊キーワードは、キーワードが表示されている場所ではなく、テンプレートがインスタンス化される場所を指す:
特別なキーワードはテンプレートの定義内に表示されるが、その値はテンプレートがインスタンス化される場所であるmain()
を参照する。(訳注: 改変前のソースでは__LINE__は12行目にある)
ファイルdeneme.dの14行目のdeneme.main関数でインスタンス化。
以下では、多次元演算子オーバーロードの例で__FUNCTION__
を使用する。
this
メンバー関数のテンプレートパラメータ
メンバー関数もテンプレートにすることができる。そのテンプレートパラメータは、他のテンプレートと同じ意味を持つ。
ただし、他のテンプレートとは異なり、メンバー関数のテンプレートパラメータは、this
パラメータにもすることができる。その場合、this
キーワードの後に続く識別子は、オブジェクトのthis
参照の正確な型を表する。(this
参照は、コンストラクタでthis.member = value
とよく書かれるように、オブジェクト自体を意味する。
OwnType
テンプレートパラメータは、メンバー関数が呼び出されるオブジェクトの実際の型である。
出力:
このオブジェクトの型 | MyStruct!int |
---|---|
const(MyStruct!int) | |
immutable(MyStruct!int) |
ご覧のとおり、型には、T
の対応する型だけでなく、const
やimmutable
などの型修飾子も含まれる。
struct
(またはclass
)はテンプレートである必要はない。this
テンプレートパラメータは、テンプレート化されていない型のメンバー関数テンプレートにも出現することができる。
this
テンプレートパラメータは、2章後に説明するテンプレートミックスインでも役立つ。
alias
テンプレートパラメータ
alias
テンプレートパラメータは、プログラムで使用される任意のシンボルまたは式に対応することができる。このようなテンプレート引数の唯一の制約は、引数がテンプレート内での使用と互換性があることだ。
filter()
map()
は、alias
テンプレートパラメーターを使用して、実行する操作を決定する。
既存の変数を変更するためのstruct
テンプレートの簡単な例を見てみよう。struct
テンプレートは、alias
パラメータとして変数を受け取る。
メンバー関数は、そのパラメータを、struct
テンプレートがインスタンス化される変数に単に代入する。その変数は、テンプレートのインスタンス化時に指定する必要がある。
そのインスタンス化において、variable
テンプレートパラメーターは変数x
に対応する:
x | y |
---|---|
10 | 2 |
逆に、MyStruct!y
でテンプレートをインスタンス化すると、variable
はy
と関連付けられる。
次に、filter()
とmap()
に類似した、呼び出し可能なエンティティを表すalias
パラメーターを定義する:
()
の括弧からわかるように、caller()
はテンプレートパラメータを関数として使っている。さらに、括弧は空なので、引数を指定せずにこの関数を呼び出すことも可能でなければならない。
この説明に一致する次の2つの関数がある。これらは、テンプレート内でfunc()
として呼び出すことができるため、どちらもfunc
を表すことができる。
これらの関数は、caller()
のalias
パラメータとして使用できる。
出力:
呼び出し: fooが呼び出された。
呼び出し: barが呼び出された。
テンプレートでの使用方法と一致する限り、任意のシンボルをalias
パラメータとして使用できる。反例として、int
変数をcaller()
で使用すると、コンパイルエラーが発生する。
コンパイルエラーは、変数がテンプレートでの使用方法と一致しないことを示している:
エラー: ()の前に関数が必要、int型の変数ではない
この間違いはcaller!variable
のインスタンス化にあるが、コンパイラにとってはvariable
を関数として呼び出そうとしていることがエラーであるため、コンパイルエラーはcaller()
テンプレート内のfunc()
を指摘する。この問題に対処する1つの方法は、テンプレート制約を使用することだ。これについては、以下で説明する。
変数が、opCall()
のオーバーロードがある、または関数リテラルであるなどの理由で関数呼び出し構文をサポートしている場合、caller()
テンプレートでも正常に動作する。次の例は、この2つのケースの両方を示している。
出力:
呼び出し: C.opCallが呼び出された。
呼び出し: 関数リテラルが呼び出された。
alias
パラメータも特殊化することができる。ただし、特殊化の構文は異なる。特殊化された型は、alias
キーワードとパラメータ名の間に指定する必要がある。
また、alias
パラメータは、テンプレート内で実際の変数の名前を使用可能にする点にも注意。
一般的な定義 | name |
---|---|
intの特殊化 | count |
doubleの特殊化 | length |
タプルテンプレートパラメーター
可変長引数の章で、可変長引数関数は任意の数の任意の型の引数を受け取ることができることを説明した。例えば、writeln()
は、任意の数の任意の型の引数で呼び出すことができる。
テンプレートも可変長である。名前の後に...
が続くテンプレートパラメータは、そのパラメータの位置に任意の数および種類のパラメータを指定できる。このようなパラメータは、テンプレート内でタプルとして表示され、AliasSeq
のように使用できる。
インスタンス化されるすべてのテンプレート引数に関する情報を単に表示するテンプレートを使って、この例を見てみよう。
テンプレートパラメータT...
により、info
は可変長のテンプレートになる。T
とargs
はどちらもタプルである。
T
は引数の型を表する。args
引数自体を表す。
次の例は、3つの異なる型の3つの値でその関数テンプレートをインスタンス化している。
次の実装は、foreach
ループで引数を反復処理して、引数に関する情報を表示するだけだ:
注釈:前章で見たように、引数はタプルであるため、上記のforeach
文はコンパイル時の foreach
である。
出力:
順番 | 型 | 引数 |
---|---|---|
0 | int | 1 |
1 | string | abc |
2 | double | 2.3 |
typeof(arg)
によって各引数の型を取得する代わりに、T[i]
を使用することもできたことに注意。
関数テンプレートでは、テンプレート引数が推測できることはわかっている。そのため、前のプログラムでは、コンパイラは型をint
、string
、double
と推測している。
しかし、テンプレートパラメータを明示的に指定することも可能だ。例えば、std.conv.to
は、宛先型を明示的なテンプレートパラメータとして受け取る。
テンプレートパラメータが明示的に指定されている場合、それらは値、型、およびその他の種類の混合であることができる。この柔軟性により、テンプレートの本体を適宜コーディングできるように、各テンプレートパラメータが型であるかどうかを判断できる必要がある。これは、引数をAliasSeq
として扱うことで実現される。
テキスト形式のソースコードとしてstruct
定義を生成する関数テンプレートで、この例を見てみよう。この関数は、生成したソースコードをstring
として返すようにしよう。この関数は、まずstruct
の名前、その後に型とメンバーの名前をペアで指定して受け取ることができる。
このstructDefinition
のインスタンス化は、以下のstring
を生成することが期待される:
注釈:ソースコードを生成する関数は、後で説明するキーワードmixin
とともに使われる。
以下は、望ましい出力を生成する実装だ。関数テンプレートがis
式をどのように利用しているかに注目。式is (arg)
は、arg
が有効な型である場合にtrue
を生成することを覚えておいてほしい。
typeof(this)
、typeof(super)
、およびtypeof(return)
場合によっては、テンプレートの汎用性により、テンプレートコード内の特定の型を把握したり、記述したりすることが困難になることがある。このような場合、次の3つの特別なtypeof
バリエーションが役立つ。これらは、この章で紹介するが、テンプレートではないコードでも機能する。
typeof(this)
は、this
参照の型を生成する。これは、メンバー関数以外でも、struct
またはclass
で動作する。typeof(super)
class
の基本型(つまり、super
の型)を生成する。typeof(return)
関数内で、その関数の戻り値の型を生成する。例えば、上記の
calculate()
関数をauto
関数として定義する代わりに、その定義の中でauto
をLargerOf!(A, B)
に置き換えることで、より明確にすることができる。(より明確にするメリットとしては、関数のコメントの一部を省略できることが挙げられる。)typeof(return)
関数本体内で戻り値の型を繰り返し記述する必要がなくなる。
テンプレートの特化
テンプレートの章でテンプレートの特殊化について見た。型のパラメータと同様に、他の種類のテンプレートパラメータも特殊化することができる。以下は、テンプレートの一般的な定義と、0に対するその特殊化だ。
テンプレート特殊化は、以下のメタプログラミングのセクションで活用する。
メタプログラミング
コード生成に関する機能であるため、テンプレートはDの高度な機能の一つだ。テンプレートは、コードを生成するコードそのものだ。コードを生成するコードを書くことをメタプログラミングと呼ぶ。
テンプレートがコンパイル時機能であるため、通常は実行時に実行される一部の操作を、テンプレートのインスタンス化としてコンパイル時に移動することができる。
(注釈:コンパイル時関数実行(CTFE)も、同じ目的を実現する機能だ。CTFEについては、後の章で説明する。)
テンプレートをコンパイル時に実行する方法は、再帰的なテンプレートインスタンス化に基づいていることが一般的だ。
この例を見るために、まず、0から特定の値までの数字の合計を計算する通常の関数を考えてみよう。例えば、引数が4の場合、この関数は0+1+2+3+4の結果を返すはずだ。
これは、関数の反復的な実装だ。同じ関数は、再帰によっても実装できる。
再帰関数は、最後の値と前の合計の和を返する。ご覧のとおり、この関数は0の値を特別に処理することで再帰を終了している。
関数は通常、実行時の機能だ。通常どおり、sum()
は実行時に実行できる。
コンパイル時に結果が必要な場合、同じ計算を行う1つの方法は、関数テンプレートを定義することだ。この場合、パラメータは関数パラメータではなく、テンプレートパラメータでなければならない。
この関数テンプレートは、last - 1
によって自身をインスタンス化し、再帰によって合計を再度計算しようとする。しかし、このコードは間違っている。
三項演算子は実行時にコンパイルされるため、コンパイル時に再帰を終了する条件チェックがない:
コンパイラはテンプレートインスタンスが無限再帰を起こすことを検出し、任意の再帰回数で停止する:
テンプレート引数4と-296の違いを考慮し、コンパイラはデフォルトでテンプレート展開を300で制限する。
メタプログラミングでは、再帰はテンプレート特殊化で終了する。0に対する次の特殊化は、期待される結果を生成する:
以下のプログラムは、sum()
をテストするプログラムだ:
現在、プログラムは正常にコンパイルされ、4+3+2+1+0の結果を生成する:
10
ここで重要な点は、関数sum!4()
は完全にコンパイル時に実行されることだ。コンパイルされたコードは、リテラル10
でwriteln()
を呼び出すことと同じだ。
その結果、コンパイルされたコードは、可能な限り高速かつシンプルになる。値10は、4+3+2+1+0の結果として計算されるが、計算全体はコンパイル時に実行される。
前の例は、メタプログラミングの利点の一つである、実行時からのコンパイル時への操作の移動を示している。CTFEは、Dのメタプログラミングの慣用表現の一部を排除している。
コンパイル時多態性
オブジェクト指向プログラミング(OOP)では、多態性は継承によって実現される。例えば、関数がインターフェースを受け取る場合、その関数は、そのインターフェースを継承する任意のクラスのオブジェクトを受け入れる。
前の章で見た例を思い出してみよう。
useSoundEmittingObject()
は多態性の恩恵を受けている。SoundEmitter
を受け取るため、そのインターフェースから派生したあらゆる型で使用することができる。
テンプレートは、あらゆる型を扱うことが本質的な機能であるため、一種の多態性を提供しているとも見ることができる。テンプレートが提供する多態性は、コンパイル時機能であるため、コンパイル時多態性と呼ばれる。一方、OOPの多態性は、実行時多態性と呼ばれる。
実際には、どちらの多態性も、型は特定の要件を満たさなければならないため、あらゆる型で使用できるわけではない。
実行時多態性では、型が特定のインターフェースを実装している必要がある。
コンパイル時多態性では、型がテンプレートによる使用方法と互換性がある必要がある。コードがコンパイルできる限り、テンプレート引数はそのテンプレートで使用できる。(注釈:引数は、オプションでテンプレート制約も満たす必要がある。テンプレート制約については、後で説明する。)
例えば、useSoundEmittingObject()
が関数ではなく関数テンプレートとして実装されていた場合、object.emitSound()
呼び出しをサポートするあらゆる型で使用することができる。
Car
は他の型と継承関係はないが、コードは正常にコンパイルされ、各型のemitSound()
メンバー関数が呼び出されることに注意。
コンパイル時の多態性は、実際の型よりも動作を重視するユーモラスな用語である"ダックタイピング"としても知られている。
コードの肥大化
コンパイラによって生成されるコードは、型のパラメータ、値のパラメータなどの引数ごとに異なる。
その理由は、int
およびdouble
を型テンプレート引数とみなすとわかる。各型は、異なる種類のCPUレジスタによって処理される必要がある。そのため、テンプレート引数が異なる場合は、同じテンプレートを別々にコンパイルする必要がある。つまり、コンパイラは、テンプレートのインスタンスごとに異なるコードを生成する必要がある。
例えば、useSoundEmittingObject()
がテンプレートとして実装されている場合、そのインスタンス化の回数だけコンパイルされることになる。
これによりプログラムサイズが肥大化するため、この効果はコードの肥大化と呼ばれている。ほとんどのプログラムでは問題にならないが、テンプレートの効果として認識しておく必要がある。
一方、useSoundEmittingObject()
のテンプレート化されていないバージョンでは、コードの繰り返しは発生しない。コンパイラは、その関数を1回だけコンパイルし、SoundEmitter
インターフェースのすべての型に対して同じコードを実行する。実行時多態性では、異なる型に対して同じコードを異なる動作させることは、バックグラウンドでの関数ポインタによって実現されている。関数ポインタは実行時にわずかなコストがかかるが、ほとんどのプログラムではそのコストは重要ではない。
コードの肥大化と実行時多態性はどちらもプログラムの性能に影響を与えるため、特定のプログラムにおいて実行時多態性がコンパイル時多態性よりも適切なアプローチであるかどうかは事前に判断できない。
テンプレート制約
テンプレートは任意の引数でインスタンス化できるが、すべての引数がすべてのテンプレートと互換性があるわけではないという事実が不便さを生む。テンプレート引数が特定のテンプレートと互換性がない場合、その互換性の欠如は、その引数に対するテンプレートコードのコンパイル時に必ず検出される。その結果、コンパイルエラーはテンプレート実装内の行を指す。
useSoundEmittingObject()
を、object.emitSound()
の呼び出しをサポートしていない型で使用して、これを確認しよう。
間違いなく、互換性のない型でテンプレートを使用しているコードにエラーがあるのだが、コンパイルエラーはテンプレート内の行を指している。
望ましくない結果として、テンプレートがサードパーティのライブラリモジュールの一部である場合、コンパイルエラーはライブラリ自体の問題のように見える。
この問題は、インターフェースでは発生しないことに注意。インターフェースを受け取る関数は、そのインターフェースを実装する型でのみ呼び出すことができる。他の型でそのような関数を呼び出そうとすると、呼び出し元でコンパイルエラーになる。
テンプレート制約は、テンプレートの誤ったインスタンス化を禁止するためのものである。これらは、テンプレート本体の直前に、 条件として論理式で定義される。if
テンプレート定義は、その制約が特定のインスタンス化においてtrue
となる場合のみ、コンパイラによって考慮される。そうでない場合、そのテンプレート定義は、その使用において無視される。
テンプレートはコンパイル時の機能であるため、テンプレートの制約はコンパイル時に評価可能でなければならない。is
式は、is
式の章で見た式で、テンプレートの制約でよく使われる。以下の例でも、is
式を使う。
単一要素のタプルパラメーター
テンプレートの単一のパラメータが、型、値、またはalias
の種類のいずれかである必要がある場合がある。これは、長さが1のタプルパラメータを使用することで実現できる。
std.traits
モジュールのテンプレートの一部は、このイディオムを活用している。std.traits
については後述する。
名前付き制約
制約が複雑で、テンプレートパラメータの要件を理解するのが難しい場合がある。この複雑さは、制約に名前を効果的に付けるイディオムによって処理できる。このイディオムは、Dの4つの機能、すなわち匿名関数、typeof
、is
式、および同名テンプレートを組み合わせている。
型のパラメータを持つ関数テンプレートでこれを見てみよう。このテンプレートは、その関数パラメータを特定の方法で使用している。
テンプレートの実装から明らかなように、この関数が扱うことができる型は、オブジェクトに対して3つの特定の関数呼び出し(prepare()
、fly(42)
、land()
)をサポートしている必要がある。
その型に対するテンプレート制約を指定する1つの方法は、テンプレート内の各関数呼び出しに対して、is
およびtypeof
式を使用することだ。
この構文については、後で説明する。ここでは、is (typeof(object.prepare()))
という構文全体が、その型が.prepare()
呼び出しをサポートしているかどうかを意味すると考えておいて。
このような制約は目的を達成するが、読みづらい場合がある。代わりに、制約全体に説明的な名前を付けることもできる:
この制約は、テンプレートが飛ぶことができる型と着陸できる型で動作するように設計されていることがより明確になったため、より読みやすくなっている。
このような制約は、以下の同名のテンプレートに類似したイディオムによって実現される:
そのイディオムに参加するDの機能と、それらの相互作用は以下で説明される:
- 匿名関数:匿名関数は、関数ポインタ、デリゲート、およびラムダの章で説明した。上記の強調表示された中括弧は、匿名関数を定義している。
- 関数ブロック:関数ブロックは、実際のテンプレートで使用される予定の型をそのまま使用する。まず、その型のオブジェクトが定義され、そのオブジェクトが特定の方法で使用される。(このコードは実行されない。以下を参照。)
- 関数の評価:匿名関数の末尾にある空の括弧は、通常、その関数を実行する。ただし、その呼び出し構文は
typeof
内にあるため、実行されることはない。 -
typeof
式:typeof
は、式の型を生成する。typeof
の重要な点は、式を実行しないことだ。むしろ、その式が実行された場合に、その式の型を生成する。前の
assert
が示すように、式++i
は実行されていない。typeof
は、その式が実行された場合にその式の型をint
として生成しただけである。typeof
が受け取る式が有効でない場合、typeof
は型をまったく生成しない (void
も生成しない)。したがって、canFlyAndLand
内の匿名関数がT
で正常にコンパイルできる場合、typeof
は有効な型を生成する。それ以外の場合、型はまったく生成されない。 -
is
式:is
式は、is
式の章でさまざまな使い方を紹介した。Type
構文は、is (Type)
構文が有効な場合にtrue
を生成する:上記の2番目の
typeof
は存在しないシンボルを受け取るが、コンパイラはコンパイルエラーを発生させない。むしろ、typeof
式は型を生成しないため、is
式はfalse
を生成する。true false
- 同名テンプレート:上記のように、
canFlyAndLand
テンプレートには同じ名前の定義が含まれているため、テンプレートのインスタンス化はその定義自体になる。
最終的に、use()
はより説明的な制約を獲得する:
このテンプレートを、制約を満たす型と制約を満たさない型の2つの型で使用しよう。
名前が付いていようがいまいが、テンプレートに制約があるため、コンパイルエラーはテンプレートが実装されている場所ではなく、使用されている行を指す。
多次元演算子オーバーロードでのテンプレートの使用
演算子オーバーロードの章で、opDollar
、opIndex
、およびopSlice
は要素のインデックス付けとスライスに使用されることを学んだ。1次元コレクションに対してオーバーロードされた場合、これらの演算子は次の役割を果たす。
opDollar
: コレクションの要素数を返す。opSlice
: コレクションの要素の一部またはすべてを表すオブジェクトを返す。opIndex
: 要素へのアクセスを提供する。
これらの演算子関数には、テンプレート化されたバージョンもあり、上記のテンプレート化されていないものとは責任が異なる。特に、多次元演算子オーバーロードでは、opIndex
はopSlice
の責任を引き継ぐことに注意。
-
opDollar
template: コレクションの特定の次元の長さを返す。次元はテンプレートパラメーターによって決定される: -
opSlice
template: 要素の範囲を指定する範囲情報を返す (array[begin..end]
内のbegin
およびend
の値など)。この情報は、Tuple!(size_t, size_t)
または同等の型として返すことができる。範囲が指定する次元は、テンプレートパラメータによって決定される。 -
opIndex
template: コレクションの一部を表す範囲オブジェクトを返す。要素の範囲は、テンプレートパラメータによって決定される:
opIndexAssign
opIndexOpAssign
にもテンプレート化されたバージョンがあり、コレクションの要素の範囲に対して操作を行う。
これらの演算子を定義するユーザー定義型は、多次元インデックスおよびスライス構文で使用できる。
このような式は、まず演算子関数を呼び出す式に変換される。変換は、$
文字をopDollar!dimension()
への呼び出しに、インデックス範囲をopSlice!dimension(begin, end)
への呼び出しに置き換えることで行われる。これらの呼び出しによって返される長さと範囲の情報は、opIndexAssign
を呼び出す際の引数として使用される。したがって、上記の式は、次のように実行される(次元値は強調表示されている)。
したがって、opIndexAssign
は引数から要素の範囲を決定する。
多次元の演算子オーバーロードの例
次のMatrix
の例は、これらの演算子を2次元型に対してオーバーロードする方法を示している。
このコードは、より効率的な方法で実装できることに注意。例えば、単一の要素eに対して操作を行う場合でも、m[i, j]
によって単一の要素のサブ行列を構築する代わりに、その要素に直接操作を適用することができる。
さらに、関数内のwriteln(__FUNCTION__)
式は、コードの動作とは何の関係もない。これらは、さまざまな演算子の使用法でバックグラウンドで呼び出される関数を公開するためにのみ使用されている。
また、次元の値の正確さはテンプレート制約によって強制されていることに注意。
要約
以前のテンプレート章では、以下の点が再確認された:
- テンプレートは、コンパイラがプログラム内の実際の使用状況に応じてインスタンスを生成するためのコードのパターンを定義する。
- テンプレートはコンパイル時機能だ。
- 関数、構造体、およびクラスの定義をテンプレートにするには、テンプレートパラメータのリストを指定するだけで十分だ。
- テンプレート引数は、感嘆符の後に明示的に指定できる。カッコ内に1つのトークンのみがある場合は、カッコは不要。
- 各テンプレートのインスタンス化は、異なる型を生成する。
- テンプレート引数は、関数テンプレートに対してのみ推測できる。
- テンプレートは、
:
文字の後の型に対して特化することができる。 - デフォルトのテンプレート引数は、
=
文字の後に指定する。
この章では、以下の概念が追加された。
- テンプレートは、完全な構文または省略構文で定義できる。
- テンプレートのスコープは名前空間である。
- テンプレートと同じ名前を持つ定義を含むテンプレートは、同名テンプレートと呼ばれる。テンプレートはその定義を表す。
- テンプレートは、関数、クラス、構造体、共用体、およびインターフェースであり、すべてのテンプレート本体は、任意の数の定義を含むことができる。
- テンプレートパラメータは、型、値、
this
、alias
、およびタプル型にすることができる。 typeof(this)
typeof(super)
、typeof(return)
はテンプレートで有用だ。- テンプレートは特定の引数に対して特化できる。
- メタプログラミングは、コンパイル時に操作を実行する方法である。
- テンプレートはコンパイル時の多態性を可能にする。
- 異なるインスタンス化に対してコードを生成すると、コードが肥大化する可能性がある。
- テンプレート制約は、特定のテンプレート引数に対するテンプレートの使用を制限する。これらは、テンプレートの実装から、テンプレートが実際に誤って使用されている場所へコンパイルエラーを移動するのに役立つ。
- テンプレート制約に名前を付けると、読みやすくなる。
opDollar
、opSlice
、opIndex
、opIndexAssign
、およびopIndexOpAssign
のテンプレート版は、多次元インデックス付けとスライシング用に用意されている。