これまでの章では、intやdouble、あるいは自作のCarクラスのように、特定の型に対して処理を行う関数やクラスを作成してきました。しかし、プログラムが複雑になるにつれて、「型は違うけれど、行いたい処理は全く同じ」という状況が頻繁に発生します。例えば、2つの値の大きい方を返すmaxという関数を考えてみましょう。
int max_int(int a, int b) {
return (a > b) ? a : b;
}
double max_double(double a, double b) {
return (a > b) ? a : b;
}
このように、型ごとに同じロジックの関数をいくつも用意するのは非効率的ですし、バグの温床にもなります。
この問題を解決するのがテンプレートです。テンプレートを使うと、具体的な型を "仮引数" のように扱い、様々な型に対応できる関数やクラスの「設計図」を作ることができます。このような、型に依存しないプログラミングスタイルを**ジェネリックプログラミング(汎用プログラミング)**と呼びます。
関数テンプレートを使うと、先ほどのmax関数の問題をエレガントに解決できます。
#include <iostream>
#include <string>
// Tという名前で型を仮引数として受け取るテンプレートを宣言
template <typename T>
T max_value(T a, T b) {
return (a > b) ? a : b;
}
int main() {
// int型でmax_valueを呼び出す
std::cout << "max(10, 20) = " << max_value(10, 20) << std::endl;
// double型でmax_valueを呼び出す
std::cout << "max(3.14, 1.41) = " << max_value(3.14, 1.41) << std::endl;
// std::string型でも動作する!
std::string s1 = "world";
std::string s2 = "hello";
std::cout << "max(\"world\", \"hello\") = " << max_value(s1, s2) << std::endl;
return 0;
}max(10, 20) = 20
max(3.14, 1.41) = 3.14
max("world", "hello") = worldtemplate <typename T>という部分が、この関数がテンプレートであることを示しています。
template <...>: テンプレートの宣言を開始します。typename T: Tという名前の「型引数」を定義しています。typenameの代わりにclassと書くこともできますが、意味は同じです。Tは、このテンプレートが実際に使われるときに具体的な型(intやdoubleなど)に置き換えられます。main関数でmax_value(10, 20)のように呼び出すと、コンパイラは引数の型がintであることから、Tをintだと自動的に判断します(これをテンプレート引数推論と呼びます)。そして、内部的に以下のようなint版の関数を生成してくれるのです。
// コンパイラが内部的に生成するコードのイメージ
int max_value(int a, int b) {
return (a > b) ? a : b;
}
同様に、doubleやstd::stringで呼び出されれば、それぞれの型に対応したバージョンの関数が自動的に生成されます。これにより、私たちは一つの「設計図」を書くだけで、様々な型に対応できるのです。
テンプレートの力は、クラスにも適用できます。これにより、様々な型のデータを格納できる汎用的なクラス(コンテナなど)を作成できます。例えば、「2つの値をペアで保持する」クラスを考えてみましょう。
#include <iostream>
#include <string>
// 2つの型 T1, T2 を引数に取るクラステンプレート
template <typename T1, typename T2>
class Pair {
public:
T1 first;
T2 second;
// コンストラクタ
Pair(T1 f, T2 s) : first(f), second(s) {}
void print() {
std::cout << "(" << first << ", " << second << ")" << std::endl;
}
};
int main() {
// T1=int, T2=std::string としてPairクラスのオブジェクトを生成
Pair<int, std::string> p1(1, "apple");
p1.print();
// T1=std::string, T2=double としてPairクラスのオブジェクトを生成
Pair<std::string, double> p2("pi", 3.14159);
p2.print();
// 違う型のPair同士は当然、別の型として扱われる
// p1 = p2; // これはコンパイルエラーになる
return 0;
}(1, apple) (pi, 3.14159)
関数テンプレートと基本的な考え方は同じですが、いくつか重要な違いがあります。
明示的な型指定:
関数テンプレートではコンパイラが型を推論してくれましたが、クラステンプレートの場合は、オブジェクトを生成する際にPair<int, std::string>のように、開発者が明示的に型を指定する必要があります。
インスタンス化:
Pair<int, std::string>のように具体的な型を指定してオブジェクトを作ることを、テンプレートのインスタンス化と呼びます。コンパイラは、この指定に基づいてT1をintに、T2をstd::stringに置き換えた、以下のような新しいクラスを内部的に生成します。
// コンパイラが内部的に生成するクラスのイメージ
class Pair_int_string { // クラス名は実際には異なります
public:
int first;
std::string second;
Pair_int_string(int f, std::string s) : first(f), second(s) {}
void print() {
std::cout << "(" << first << ", " << second << ")" << std::endl;
}
};
Pair<int, std::string>とPair<std::string, double>は、コンパイルされると全く別のクラスとして扱われることに注意してください。
クラステンプレートは、C++の強力なライブラリである**STL (Standard Template Library)**の根幹をなす技術です。次章で学ぶvectorやmapといった便利なコンテナは、すべてクラステンプレートで実装されています。
< >内に具体的な型を明示的に指定してインスタンス化する必要があります。テンプレートを使いこなすことで、コードの再利用性が劇的に向上し、より柔軟で堅牢なプログラムを記述できるようになります。
任意の型の配列(ここではstd::vectorを使いましょう)を受け取り、その要素をすべて画面に出力する関数テンプレートprint_elementsを作成してください。
#include <iostream>
#include <vector>
#include <string>
// ここに関数テンプレート print_elements を実装してください
int main() {
std::vector<int> v_int = {1, 2, 3, 4, 5};
std::cout << "Integers: ";
print_elements(v_int);
std::vector<std::string> v_str = {"C++", "is", "powerful"};
std::cout << "Strings: ";
print_elements(v_str);
return 0;
}Integers: 1 2 3 4 5 Strings: C++ is powerful
後入れ先出し(LIFO)のデータ構造であるスタックを、クラステンプレートSimpleStackとして実装してください。以下のメンバ関数を持つようにしてください。
void push(T item): スタックに要素を追加するT pop(): スタックの先頭から要素を取り出すbool is_empty(): スタックが空かどうかを返すstd::vectorを内部のデータ格納場所として利用して構いません。int型とchar型で動作を確認してください。
#include <iostream>
#include <vector>
#include <stdexcept>
// ここにクラステンプレート SimpleStack を実装してください
int main() {
SimpleStack<int> int_stack;
int_stack.push(10);
int_stack.push(20);
std::cout << "Popped from int_stack: " << int_stack.pop() << std::endl; // 20
std::cout << "Popped from int_stack: " << int_stack.pop() << std::endl; // 10
SimpleStack<char> char_stack;
char_stack.push('A');
char_stack.push('B');
std::cout << "Popped from char_stack: " << char_stack.pop() << std::endl; // B
std::cout << "Popped from char_stack: " << char_stack.pop() << std::endl; // A
return 0;
}Popped from int_stack: 20 Popped from int_stack: 10 Popped from char_stack: B Popped from char_stack: A