テンプレート (プログラミング)

プログラミングにおけるテンプレートは、静的型付けプログラミング言語データ型を抽象化してコードを書くことを可能にする機能であり、C++D言語においてはジェネリックプログラミングに用いられる。

C++のテンプレートは後から追加された機能だが、多重継承演算子多重定義と並ぶ重要な機能となった。STL (Standard Template Library) はテンプレートによって構築された汎用的なアルゴリズムやデータ構造を含むソフトウェアフレームワークとなっている。

C++

概要

C++11規格までのテンプレートには、大別して関数テンプレートクラステンプレートがある。C++14では変数テンプレートもサポートするようになった。

関数テンプレートは任意の型の引数を受け取ることができる関数のようなものである。たとえば、標準C++ライブラリにある関数テンプレートmax(x, y)は引数xyのうち、大きな方を返す関数テンプレートで、次のように定義できる。

template <typename T>
const T& max(const T& x, const T& y)
{
    if (x < y)
        return y;
    else
        return x;
}

そして次のように呼び出せる。

using namespace std;
cout << max<int>(3, 7) << endl; // 7が出力される。
cout << max(3, 7) << endl; // max(int, int) から max<int>(const int&, const int&) に推論される。
cout << max(3, 7L) << endl; // int, long の曖昧さが解決できないためコンパイルエラー。
cout << max<double>(-3.1, -8) << endl; // -3.1が出力される。
cout << max(-3.1, -8.0) << endl; // max(double, double) から max<double>(const double&, const double&) に推論される。
cout << max(-3.1, -8) << endl; // double, int の曖昧さが解決できないためコンパイルエラー。

ここで、テンプレート内にてTで表されているものをテンプレート仮引数 (template parameter) という。Tはパラメータ化された型 (parameterized type) とも呼ばれる。テンプレート仮引数はtypenameキーワードもしくはclassキーワードを用いて修飾する必要がある。テンプレート自身は実体をもたないため、利用する際は実体化 (instantiate) する必要がある。実体化の方法のひとつとして、max<int>のように具体的な型すなわちテンプレート実引数 (template argument) を明示的に指定することができるが、曖昧さがない場合に限り、コンパイラは関数テンプレートの実引数として与えられた値の型(この例の場合では、xおよびyとして渡された整数型リテラルの型であるint)からTに対応する型を推論し、得られた具体的な型intTに当てはめて関数を暗黙的に実体化することもできる。このようにして実体化された関数を特殊化(あるいは特殊化版、特殊バージョン)という。例のmax<int>(3, 7), max(3, 7)に対してはintの特殊化が、max<double>(-3.1, -8), max(-3.1, -8.0)に対してはdoubleの特殊化が呼ばれることになる。なお、実体化のことを具現化と呼ぶこともある。

maxのインスタンス化に際して、xyがどんな型でもx < yという式が生成される。またユーザー定義型の場合も、<演算子が適切に多重定義されていればその型に対してmaxが使える。Tとして利用可能な型の集合に何らかの共通の継承関係がある必要はない。つまり、テンプレートは静的なダック・タイピング (duck typing) を可能とする。同様の例は他にもあり、STLには適切な演算子が定義されていれば任意の型が利用可能になるという関数テンプレートが数多く存在する。例えば<演算子が定義されていれば、sort(), stable_sort(), binary_search()や、配列からヒープ構造を作成するアルゴリズム[要説明]setなどのコンテナなどが利用可能になる。関数テンプレートには述語 (predicate) として関数オブジェクトを受け取るものも存在し、ソートの比較などに使われるコールバック関数をカスタマイズすることが容易になっている。つまり、要件を満たすためのいくつかの演算子を定義するだけで、新しいデータ型に対して幅広い機能を得られる。

C++のテンプレートは完全にコンパイル時点でタイプセーフである。例えば、複素数には狭義の順序 (strict order) がないので、標準C++ライブラリの複素数型であるcomplexには<演算子が定義されていない。従って、前述のmax関数テンプレートにcomplex型を指定した場合、コンパイルエラーとなる。同様に、<をあてにする他のテンプレートはcomplexデータに適用できない。ただ、通例コンパイラはこの種のエラーに対して、難解でほとんど役に立たないエラーメッセージを出力する。テンプレートが関わるエラーを出さないように、プログラマーは利用する関数テンプレートの仕様をあらかじめよく確認しておくしかない。

一般的に、そのようなテンプレート実引数に対する要求は、コンセプト (concept) や要件 (requirement) などとして別途文書によって記述されている。標準C++ライブラリのstd::max関数テンプレート (X3014 25.3.7) では、そのテンプレート実引数がLessThanComparableでなければならないと定められている。ある型がLessThanComparableであるためには、正にa < bという演算ができなければならない (X3014 20.1.2) と定められているのである。

テンプレートに関するコンパイルエラーメッセージを改善するために、C++0xではコンセプトと呼ばれる機能の導入が検討されていたが、結局C++11では見送られた。コンセプトはC++17規格でも実装されていない。

クラステンプレートはテンプレートの概念をクラスに当てはめたものである。汎用的なコンテナ (generic containers) の作成によく用いられる。たとえばSTLにはリンクリストとしてstd::listが存在する。整数のリストを作りたければstd::list<int>と書き、文字列のリストを作りたければstd::list<std::string>と書けばよい。std::listの要素型に対する要件は規格バージョンによって異なるが、例えばC++03まではCopyAssignableおよびCopyConstructibleを必要とする[1]。これらの要件を満たす限り、たとえどんな型がstd::listの要素型として指定されても動作する共通の関数集合が定義されている。なお、関数テンプレートと違い、クラステンプレートではテンプレート引数を型推論することができなかったが、C++17にてサポートされるようになった。

テンプレート引数の数はC++03までは固定であり、定義時にひとつひとつ明示的に指定する必要があったが、C++11では可変引数テンプレート (variadic template) がサポートされるようになった。可変長引数の関数やマクロのように、...という記法を用いて記述する。

テンプレートの明示的特殊化と部分特殊化

テンプレートの明示的特殊化 (explicit specialization) は、テンプレート引数について、特定の型に対する明示的個別実装を可能とする機能である。テンプレートの明示的特殊化は、特定の形式に最適化できるようにすることと、コード肥大化の削減に役立てることという2つの目的がある。

例としてsort()関数テンプレートについて考える。このような関数の動作は第一にコンテナの特定位置の2つの値を入れ替えまたは交換することである。値が多い場合(各要素を格納するためにメモリを消費するという点で)、オブジェクトへのポインタのリストを先に構築し、それらのポインタをソートして、最終的なソートされたシーケンスを構築するのが多くの場合で高速である。値が非常に少なければ単純に必要に応じて値をスワップで置き換えるのが最も速い[要出典]。しかしさらにパラメータ化された型がポインタ型である場合はポインタの配列を構築する必要がない。テンプレートの明示的特殊化はテンプレートの実装者が複数の異なる実装を記述することを可能にし、パラメータ化された型が各実装で利用されるべき特徴を指定できるようにする。

テンプレートの部分特殊化 (partial specialization) は、テンプレート仮引数の一部を特殊化するものである。明示的特殊化と異なる点は、特殊化されず抽象化されたままのテンプレート仮引数を持つことである。

テンプレートメタプログラミング

C++のテンプレートはまた、実行時ではなくコンパイル時にコードの一部を事前評価する方法であるテンプレートメタプログラミングにも利用できる。C++のテンプレートはチューリング完全である。ただし、コンパイラによる制限がある。規格上は再帰の深さは参考 (informative) で処理系限界 (implementation limits / implementation quantities) であり、C++の作業原案 (working draft) のひとつ N3797 では1024を最小として例示している[2]。再帰の制限が無ければコンパイル処理が永遠に終了しないコードを記述することができ、それをコードから判断することはコンパイラには不可能だからである。停止性問題を参照。

利点と欠点

テンプレートの用法の中にはC言語プリプロセッサマクロで代替できるものがある。例えばmax関数テンプレートの代替として、次のようにMAXマクロを定義できる。

#define MAX(a, b) ((a) < (b) ? (b) : (a))

マクロもテンプレートもコンパイル時までに展開される。マクロは常にインライン展開されるが、関数テンプレートはコンパイラの判断によってインライン関数として扱われる。そのため(関数テンプレートがインライン展開される限り)実行時のオーバーヘッドは両者共に発生しない。

しかしテンプレートはマクロに比べて型安全であるという点で大きな違いがある。関数形式のマクロでよく起こるエラーをテンプレートでは回避できる。関数テンプレートでは、関数形式マクロのように引数の意図しない多重評価は起こらない。そしてマクロのおそらく重大な欠点として1論理行に収めなければならないという制限があり、規模の大きいマクロは記述が面倒ということがある。テンプレートはマクロよりも大規模なコードに対して特に効果的である。また、マクロは常にグローバル名前空間を汚染するが、テンプレートは任意の名前空間あるいは構造体やクラスの内部に定義することができる。

一方、テンプレートには大きく三つの欠点がある。それはコンパイラのサポート、貧弱なエラーメッセージ、コードの肥大化である。

まず、歴史的に多くのコンパイラの対応が貧弱だったため、テンプレートを使うと移植性が低下する恐れがある。C++について考慮していないリンカを利用するとき、あるいは共有ライブラリの境界を越えてテンプレートを使おうとしたときも十分な対応がなされていない処理系があった。C++98以降はテンプレートを含めた規格標準化とコンパイラ対応が進み、改善されている。

次に、ほとんど全てのコンパイラは、テンプレートを使ったコードにエラーを検出したときに、混乱を誘発するような、長く、そして役に立たないエラーメッセージを出力する。これはテンプレートを使った開発を難しくしている。

最後に、テンプレートは実体化される度にコードが生成されていくため、無秩序に使っていくとコードの膨張を引き起こし実行ファイルが大きくなってしまう問題がある。しかしながら一部のケースでは、うまくテンプレートの特殊化を利用することで、そのようなコードの肥大化を劇的に減らすことができる。そのほか、実体化されたもののうち、完全に同じテンプレート実引数に対するコードは共有するように最適化されたコードを出力するコンパイラを利用することで改善できるが、これはインライン化によるオーバーヘッド低減とのトレードオフでもある。また、テンプレートによって生じる余分なインスタンス化はデバッガが素直にテンプレートを扱うことを困難にする原因ともなりうる。例えばソースコードのテンプレートの中にブレークポイントをセットする場合、実際のインスタンスの中の望ましい場所にセットすることに失敗するかもしれないし、そのテンプレートによってインスタンス化された全ての場所にブレークポイントを設置しなければならないかもしれない。

テンプレートは実体化に際して完全な定義が必要である。非テンプレートの関数やクラスのように、宣言部をヘッダーファイルに記述し、実装部をソースファイルに分離することはできない。C++03にはexportキーワードによるテンプレートのエクスポート機能が存在していたが、C++11では削除された。

他言語

C++を基にしたJavaC#では、C++のテンプレートが引き起こした問題を避けるためテンプレートは除外された。しかし、後にテンプレートに似た機能(ジェネリクス; generics)を導入してジェネリックプログラミングに適応しようとしている。

D言語にもテンプレートは存在するが、C++のものより更に改良を加えられている。例えば、テンプレート引数として文字列や浮動小数点数、さらには変数や関数などのシンボルを渡すことができる。これにより、高階関数のテンプレート引数に文字列として渡された式をインライン化するといった手法が可能になっている。その他にも、テンプレート制約を指定できる組み込みの構文や、テンプレート引数によって実体化されるコードを変化させられるstatic if等の機能が用意されており、中にはD言語の開発メンバーなどによってC++への取り込みが提案されているものもある。

AdaはC++がテンプレートを採用する以前からジェネリクス(汎用体)を持っていたが、それはテンプレートの特殊化が行なえないなどC++のテンプレートに比して機能は制限されたものであり、C++のようなテンプレートメタプログラミングを行なうことはできなかった。

参考文献

脚注

関連項目