テンプレート制約
テンプレートの多重定義は、通常、 実テンプレート引数が仮引数にマッチするかどうかという基準で解決されます。 テンプレート仮引数を特殊化することで、 特定の型パターンにしかマッチしないように記述することが可能です。 同様に、 テンプレートの値引数が特定の型の物しかこないように制限することも可能です。
しかし、これにはまだ制限があります。 テンプレートとして受け取る物を、 任意の複雑な条件で制約したいことは多々あります。 例えばこんな用途が考えられるでしょう:
- 引数に応じて、 より精細な条件で使うテンプレートのインスタンスを切り替える。
- テンプレート引数が満たさなければならない条件の、 よりよいドキュメンテーションとして使う。
- 引数がマッチしない場合に、 (ユーザーには)無関係なテンプレートの内部実装の詳細に基づいた汚いエラーメッセージではなく、 わかりやすい情報を表示する。
テンプレート制約は、 これらの要求に簡潔に答えるために指定できる、 引数のマッチングが行われた後にコンパイル時評価するとtrueにならなければならない式です。 この式がtrueになると初めてそのテンプレートが引数に対してマッチしたことになり、 そうでなければ、マッチせず、 オーバーロード解決の過程から捨てられます。
制約式は、テンプレート宣言のあとに続けて、 予約語 if の後に記述します:
template Foo(int N)
if (N & 1)
{
...
}
この例では、Foo は引数が奇数だった時のみマッチします。 コンパイル時計算可能である限り、どんな複雑な条件を記述することも可能です。 例えば、 以下の例は素数だけを受け取るテンプレートです:
bool isprime(int n)
{
if (n < 1 || (n & 1) == 0)
return false;
if (n > 3)
{
for (auto i = 3; i * i < n; i += 2)
{
if ((n % i) == 0)
return false;
}
}
return true;
}
template Foo(int N)
if (isprime(N))
{
...
}
Foo!(5) // ok, 5 は素数
Foo!(6) // Foo にマッチしない。
型の制約にも複雑な条件を用いることができます。例えば、 以下のテンプレート Bar は古くからあるテンプレートの特殊化で浮動小数点型のみにマッチする例ですが:
template Bar(T:float)
{
...
}
template Bar(T:double)
{
...
}
template Bar(T:real)
{
...
}
これではテンプレートの実装を複数箇所に同じ事を書かねばなりません。 テンプレート制約を使えば、 一つのテンプレートにまとめることができます:
template Bar(T)
if (is(T == float) || is(T == double) || is(T == real))
{
...
}
標準ライブラリ std.traits の isFloatingPoint を使うともっと簡単に書くことが可能です:
import std.traits;
template Bar(T)
if (isFloatingPoint!(T))
{
...
}
型の性質についても調べることができます。 例えば、足し算ができる型がどうかのチェックはこうなります:
// T のインスタンスが足し算可能なら true
template isAddable(T)
{ // 型 T の2つのインスタンスを足し合わせてみる
const isAddable = __traits(compiles, (T t) { return t + t; });
}
int Foo(T)(T t)
if (isAddable!(T))
{
return 3;
}
struct S
{
void opAdd(S s) { } // 加算が定義された構造体
}
void main()
{
Foo(4); // 成功
S s;
Foo(s); // 成功
Foo("a"); // マッチ失敗
}
コンパイル時計算可能などんな式も制約に使えるので、 複数の制約を合成することもできます:
int Foo(T)(T t)
if (isAddable!(T) && isMultipliable!(T))
{
return 3;
}
より複雑なタイプの制約としては、 型に関して必要な操作を列挙するというものがあります。例えば、以下の isStack はスタックとして使うのに必要な操作を指定しています:
template isStack(T)
{
const isStack =
__traits(compiles,
(T t)
{ T.value_type v = top(t);
push(t, v);
pop(t);
if (empty(t)) { }
});
}
template Foo(T)
if (isStack!(T))
{
...
}
制約では複数のパラメタを扱うこともできます:
template Foo(T, int N)
if (isAddable!(T) && isprime(N))
{
...
}
制約によるオーバーロード
同じ名前でオーバーロードされたテンプレートの一覧があるときに、 テンプレート制約はその一覧を yes/no でふるい落としてマッチの対象を絞り込む働きをします。 制約によってオーバーロードを制御するには、したがって、 互いに排他的な制約条件を記述することで行います。 例えば、Foo を奇数と偶数の場合で場合分けするには、 以下のようになります:
template Foo(int N) if (N & 1) { ... } // A
template Foo(int N) if (!(N & 1)) { ... } // B
...
Foo!(3) // A が使われる
Foo!(64) // B が使われる
制約は、複数のインスタンスのどちらがより制約されているか、 という評価には使用されません。
void foo(T, int N)() if (N & 1) { ... } // A
void foo(T : int, int N)() if (N > 3) { ... } // B
...
foo!(int, 7)(); // B がより制約されているので使われる
foo!(int, 1)(); // A が使われる。B の制約が満たされないため。
foo!("a", 7)(); // A が使われる。
foo!("a", 4)(); // エラー、マッチしない。
参考文献
- Concepts (Revision 1) by Douglas Gregor and Bjarne Stroustrup