C++によるプログラミングに関する世界観2(ジェネリック)です。
テンプレートを使うことで、「どんな型にでも汎用的に対応できる型」を作ることができる。特定の型に依存しないアルゴリズムを書くことができるため、「int型でもchar型でもどちらでも使えるようなリスト型データ構造を作りたい」などといった場合に使う。
C++に標準ライブラリとして備わっているSTLでは、テンプレートを多用することでさまざまな型に対応してアルゴリズム・コンテナ・反復子などを使うことができる。C++3(STL・ライブラリ)を参照のこと。
後日注記:C++のテンプレートには注意が必要。効率のためにコードが冗長に作成される(コンパイラはテンプレートの実体化のたびにコードを生成する)ため、バイナリのファイルサイズが大きくなり、デバッグも難しくなる(エラーメッセージも役に立たないことが多い)。
テンプレートは、一言で言えば型を作る型です。「どんな型でも一般的に使うことができる」という理由で、「Generics」(一般とか総称という意味)という名称で呼ばれます。
ジェネリックプログラミングは、C#やJavaでも見られる考え方です。要するに、Listと一言に言っても、StringのListやIntegerのListがあります。こうしたものを、総称的に使うことができるのです。
これらは、動的スクリプト言語ではあまり言われない考え方です。それは、動的スクリプト言語では、そもそも型を動的に扱うため、リストやタプルの中でさまざまな型を「ミックスして」使うことができるからです。
テンプレートは最初はList<String> lst;などと書く必要があり、とまどうかもしれません。また、C++のSTLではこうしたジェネリックプログラミングの考え方を多用し、「テンプレートメタプログラミング」という全くありえない考え方で、スマートにさまざまなコンテナやアルゴリズムを操作することができます。全ては繋がっています。C++3(STL・ライブラリ)を参照のこと。
後日注記:僕がここで「型を作る型」を表現したのは、その場その場の状況に応じて、型を自分で決めて「作る」ことができるからです。最初から存在しない型やクラス、ユーザが後で自分の型として作った型やクラスに対応することもできます。実際は、テンプレートとは「汎用的にどんな型に対しても対応できるロジックを書くことができるクラス」ということです。すなわち、intとfloatにどちらでも対応できるロジックとか、どんな型のデータであっても(それが後で作られる「今存在しないクラス」であったとしても)詰め込むことのできるベクター配列やリストデータ型とか、そういうもののためにテンプレートは使用できます。
テンプレートとは、どんな型でも使用できる汎用的な型を含むクラスや関数のこと。
C++でのテンプレートでは、必要に応じて必要な型を指定し、クラスをある型のためにその都度作って使うというやり方ができる。
たとえば、汎用関数は以下のように定義する。
template <typename T> T t_add(T x, T y) { return x + y; }
この関数にint型の引数を与えると、int型の返り値を返す。また、float型など、int以外の型であっても、汎用的に関数を使うことができる。
また、汎用クラスは以下のように定義する。
template <class T> class TCalc { private: T t_val1; T t_val2; public: TCalc(T a_val1, T a_val2) { t_val1 = a_val1; t_val2 = a_val2; } T add() { return t_val1 + t_val2; } T sub() { return t_val1 - t_val2; } T mul() { return t_val1 * t_val2; } T div() { return t_val1 / t_val2; } };
汎用クラスのオブジェクトは以下のように作成する。
TCalc<int> t_calc1(14, 20); cout << t_calc1.add() << endl; cout << t_calc1.sub() << endl; cout << t_calc1.mul() << endl; cout << t_calc1.div() << endl; TCalc<float> t_calc2(40.53, 12.98); cout << t_calc2.add() << endl; cout << t_calc2.sub() << endl; cout << t_calc2.mul() << endl; cout << t_calc2.div() << endl;
ここでTの型は、どんな型、たとえば数値や文字列やあらゆるクラスのオブジェクトに、宣言された時の型の情報に基づいて「変貌」する。数値型であっても、文字列型であっても成り立つような、何にでもなれるデータ型になる。
動的スクリプト言語との違いは、使う前に必ずTClass<int>のように事前に型を明示的に指定しなければならない。
Javaのようなオブジェクト指向言語では、Object型が全てのクラスの親クラスとなっており、Object型に全てのオブジェクトを詰め込むことができる。感覚としては近いかもしれないが、テンプレートは継承環境にない型やクラスであっても、どんな型でも成り立つ。
C++の標準ライブラリに含まれるSTL(Standard Template Library)では、どんな型でも出し入れすることのできるコンテナや、どんな型でも順番に処理することができるイテレータなどのシーケンスにテンプレートが使われている。
2023.01.20編集
自分の書いたブログ「未来のわたしの心より今のあなたへ」2021/03/27より。
テンプレートにおいて、typenameとclassはほとんど同じ意味。
普段使う上ではどちらを使っても構わない。
typenameはintなどの基本型、classはクラスなどの目的に使うことを期待している。
詳しくは、Effective C++ 第3版が参考になります。
テンプレートは効果的に使いましょう。
たとえば、スマートポインタを実装したり、関数オブジェクト(functorやlambda式)を作ったり、Singletonパターンを実装したりする際にも、テンプレートを使うことができます。
詳しくは、やねうらお氏による「Windowsプロフェッショナルゲームプログラミング」「Windowsプロフェッショナルゲームプログラミング2」が参考になります。
後日注記:やねうらお氏の上記の書籍は、まるで「C++テンプレート・テクニック集」のように、テンプレートを使った高度なテクニックが満載で、めまいがでるほど参考になります。
後日注記:テンプレートは、あらゆるクラスのオブジェクトのために使うことのできるようなラッパーとして使うことがあります。特に、リソースの管理のため(newとdeleteを上手く行い、コンストラクタとデストラクタを上手く呼び出すため)に、テンプレートを一枚被せてオブジェクトのリソース作成と破棄を管理したりすることがあります。この応用がスマートポインタです。最近のC++では、スマートポインタが標準で提供されているため、このようなリソース管理専用のテンプレートを書く必要は必ずしもありません。スマートポインタを使いましょう。やねうらお氏の上記の書籍が参考になります。
C++のクラスやオブジェクトはダックタイピングではないが、テンプレートに限って言えば、RubyやPythonで言うようなダックタイピングと、テンプレートは似たような仕組みであると言える。
たとえば、RubyやPythonのような動的型付け言語において、さまざまなクラスや型の変数にあるかどうか分からない.show()メソッドを呼びだすように、C++のテンプレートでは、「あるかどうか分からないメソッドやインターフェースを暗黙的に使用」します。
Effective C++ 第3版では、これを「実行時ポリモーフィズム」と「コンパイル時ポリモーフィズム」と呼びます。C++のオブジェクト指向プログラミングは「実行時ポリモーフィズム」であり、明示的かつ実行時に仮想関数のクラスのインターフェースが決定される。それに対して、テンプレートジェネリックプログラミングは「コンパイル時ポリモーフィズム」であり、暗黙的かつコンパイル時にテンプレートのインターフェースが関数のオーバーロードによって決定される。
C++のジェネリクスで「T」という識別子を使う理由は、型が二種類以上になることがあるからです。
たとえば、ハッシュをジェネリクスにする際に、キーの型と値の型はそれぞれ異なる型を使用するべきです。
なので、T以外にEやKやVを増やすことができるように考えられているのです。
2023.05.14
Java(コレクションと配列)に、ジェネリクスに関連する内容があります。