一度書けば、どんな型でも使える
前のページでは std::sort を使って vector<int> をソートしました。しかし std::sort は vector<string> も vector<double> も、さらには自作の構造体の配列でもソートできます。しかも、それぞれに別々の sort を誰かが書く必要はありません。これは魔法でもオーバーロードでもありません。テンプレート です。コンパイラが、渡された型が何であれ再利用する、たった 1 つのコードです。
テンプレートがなければ、型ごとに同じロジックをコピー&ペーストし続けるはめになります。次に示すのは、同じ maximum 関数を 3 回書いたもので、まさにテンプレートが消し去るために存在する重複です。
int maximum(int a, int b) { return a > b ? a : b; }
double maximum(double a, double b) { return a > b ? a : b; }
string maximum(string a, string b) { return a > b ? a : b; }
本体は同一です。違うのは型だけ。テンプレートを使えば「これは任意の型 T で動く」と言い、それを一度書くだけで済みます。
関数テンプレート
関数の前に template <typename T> を付け、本来は具体的な型が来る場所すべてに T を使うことで、関数をテンプレートにします。
一度も maximum<int> や maximum<double> と書いていないことに注目してください。コンパイラは引数を見て T が何であるべきかを判断します。これが テンプレート引数推論 です。呼び出す型が異なるたびに、コンパイラは舞台裏で別々の具体的な関数を インスタンス化(生成)します。
推論が役に立たない場合は、山かっこを使って型を明示的に指定 できます。
推論にはよくある落とし穴が潜んでいます。T は 単一の 型でなければならないため、引数の型を混ぜると推論が壊れます。
maximum(3, 7.5); // エラー: T は int か double か? コンパイラは推測を拒否します。
これは明示的に指定すれば直せます(maximum<double>(3, 7.5))。あるいは各パラメータにそれぞれの型パラメータを与える方法もあり、次にそれを行います。
複数の型パラメータ
テンプレートは 1 つの型に限られません。必要なだけカンマで区切って並べられます。パラメータが異なる型になり得る関数は、次のように書きます。
戻り値の型がパラメータに依存する場合は、auto(C++14 以降)を使ってコンパイラに導出させましょう。これはテンプレートと自然に組み合わさります。
クラステンプレート
テンプレートは関数だけのものではありません。クラス 全体もテンプレート化できます。標準コンテナはまさにこの仕組みで動いています。vector<int>、map<string, int>、pair<A, B> はすべてクラステンプレートです。データ構造を一度書けば、パラメータとして指定した型を何でも格納できます。
次に示すのは、任意の型の値を 1 つ保持する小さなジェネリックな Box です。
関数テンプレートとの大きな違いは、クラス テンプレートでは通常、型を山かっこで指定する必要があることです(Box<int>)。古い規格では、そこから推論できるコンストラクタ引数がないからです。(C++17 で クラステンプレート引数推論 が追加され、Box b(42); も動くようになりましたが、明示するのは常に安全で、読みやすくもあります。)
エラーは膨大になる — その理由
ここは誰もがつまずく部分なので、はっきり言っておきます。テンプレートが完全にチェックされるのは、実際の型で インスタンス化 されたときだけです。< を使うテンプレートを書いても、それ単体では問題なくコンパイルできます。エラーは、< を持たない型でインスタンス化したその瞬間に初めて現れます。
template <typename T>
T maximum(T a, T b) {
return a > b ? a : b; // T が > をサポートしている必要がある
}
struct Point { int x, y; };
// maximum(Point{1,2}, Point{3,4});
// エラー: Point には operator > がありません。メッセージは Point を名指しし、
// さらにこの関数全体を引用するため、しばしば何行にもわたります。
コンパイラは完全な型をテンプレートに代入し、生成されたコードの 内部から 失敗を報告するため、たった 1 つのミスがライブラリ内部に言及する出力の壁を生み出すことがあります。生き残るための 2 つのコツ:
- 最初の エラーを読みましょう。最後のではありません。後続のエラーはたいてい最初のエラーの余波です。
- メッセージの中に 自分自身の型名(ここでは
Point)を探しましょう。それで、どのインスタンス化が失敗したかが分かります。
本当の解決策は、テンプレートが必要とするものを自分の型がサポートするようにすることです。maximum の場合、それは Point に operator> を与えることを意味しますが、これは後のページのテーマです。モダンな C++20 の コンセプト はこれらのエラーをより早い段階に移し、読みやすくできますが、その下にある置換のモデルは同じです。
次へ: クラス
テンプレートに集中しながら、あなたはたった今 Box クラステンプレート — プライベートなデータ、コンストラクタ、メンバ関数を持つクラス — を作りました。次のページではペースを落とし、クラス をきちんと教えます。データと、それを操作する関数をどうまとめるか、public と private が実際に何を制御するのか、そしてメンバ関数がオブジェクト自身の状態にどうアクセスするのか。実際の C++ ではテンプレートとクラスが絶えず組み合わさるので、クラスをしっかり理解しておくと、ジェネリックなコードを書くのがずっと楽になります。
よくある質問
C++ のテンプレートとは何ですか?
テンプレートとは、関数やクラスを一度だけ書けば、使う型ごとにコンパイラがそのバージョンを生成してくれる「ひな形」です。template <typename T> と書き、本来の型の代わりに T を使います。コンパイラは具体的なバージョンを生成します。これを インスタンス化 と呼びます。
C++ テンプレートにおける typename と class の違いは何ですか?
typename と class の違いは何ですか?テンプレートのパラメータリストでは、template <typename T> と template <class T> は まったく同じ意味 です。今日では一般に typename が好まれます。T はクラスに限らずあらゆる型になり得るので、そのほうが正直に読めるからです。どちらのキーワードを選んでも、生成されるコードには何の影響もありません。
C++ のテンプレートのエラーメッセージはなぜあんなに長いのですか?
テンプレートは、書いたときではなく、実際の型で インスタンス化 されたときにチェックされます。ある型が、あなたが使った操作(ソート用の < など)をサポートしていないと、インスタンス化された型がすべて展開された状態でライブラリコードの奥深くにエラーが現れ、何ページもの出力になります。最初の エラーを読み、その中に 自分の 型名を探しましょう。