第49回 クラステンプレート
型をパラメータに取るクラス。std::vector<T>、std::pair<K, V>、std::unique_ptr<T> ― STL の多くの型はクラステンプレートです。自作で Box<T> や Stack<T> を作る練習で理解を深めましょう。
このページで押さえること
✅ 最低限ここだけ覚える
template<class T> class Foo { ... }; で宣言
- 使うときは
Foo<int> のように型を明示
- メンバ関数もテンプレート引数 T を使える
- C++17 以降は型推論(CTAD)も使える
⭐ 余裕があれば読む
- 部分特殊化(特定の型だけ違う実装)
- テンプレートメンバ関数
- テンプレートの別ファイル分離は事実上不可
- C++17 の CTAD(コンストラクタからの型推論)
1. まず触ってみる ― Box<T>
「1 つの値を格納する箱」を型に依存せず作るテンプレート:
Box<T>クラステンプレート
template<class T>
class Box {
T value_;
public:
explicit Box(T v) : value_(std::move(v)) {}
T& get() { return value_; }
const T& get() const { return value_; }
void set(T v) { value_ = std::move(v); }
};
// 使う:型を明示
Box<int> b1{42};
Box<std::string> b2{"hello"};
Box<double> b3{3.14};
// C++17 以降は CTAD で型推論も可能
Box b4{42}; // Box<int> と推論
Box b5{std::string{"hi"}}; // Box<std::string>
ここまでで覚えること(3 つ):
- クラスの宣言の前に
template<class T>
- クラス内で T を任意の型として使う
- 使うときは
ClassName<型>(CTAD で省略できることも)
2. Stack<T> を作ってみる
もう少し実用的な例として、シンプルなスタック:
Stack<T>実装例
template<class T>
class Stack {
std::vector<T> data_;
public:
void push(T v) { data_.push_back(std::move(v)); }
void pop() { data_.pop_back(); }
T& top() { return data_.back(); }
bool empty() const { return data_.empty(); }
size_t size() const { return data_.size(); }
};
Stack<int> s;
s.push(1); s.push(2); s.push(3);
std::cout << s.top(); // 3
s.pop();
std::cout << s.top(); // 2
メンバ関数の外部定義
クラス外で定義冗長
template<class T>
class Stack {
public:
void push(T v); // 宣言のみ
};
// 外部で定義するときも template が必要
template<class T>
void Stack<T>::push(T v) {
// 実装
}
外部定義は冗長なので、クラス内定義のほうが一般的。どちらにしても同じヘッダファイルに書く必要があります。
3. 複数の型パラメータ
Pair<K, V>std::pair 風
template<class K, class V>
struct MyPair {
K first;
V second;
};
MyPair<std::string, int> p{"Alice", 42};
std::cout << p.first << ": " << p.second;
デフォルト引数
デフォルト型便利
template<class T, class Allocator = std::allocator<T>>
class MyVector { /* ... */ };
MyVector<int> v1; // Allocator はデフォルト
MyVector<int, CustomAllocator> v2; // 明示指定
4. 特殊化
「特定の型だけ違う実装にしたい」ときにテンプレートの特殊化を使います。
全特殊化特定型のみ
// 一般形
template<class T>
struct TypeName {
static std::string get() { return "unknown"; }
};
// int 専用
template<>
struct TypeName<int> {
static std::string get() { return "int"; }
};
// double 専用
template<>
struct TypeName<double> {
static std::string get() { return "double"; }
};
TypeName<int>::get(); // "int"
TypeName<double>::get(); // "double"
TypeName<char>::get(); // "unknown" (一般形)
「std::vector<bool> がビットパックされている」のも部分特殊化の応用例(良い実装例ではないという評価はあるものの)。
⭐
ここまでで基本は OK
最後は CTAD。C++17 で入った便利機能。
5. CTAD(クラステンプレート引数推論)
C++17 から、コンストラクタの引数から型を自動推論できるようになりました。
C++14 まで明示必須
std::pair<int, std::string> p{1, "hi"};
// make_pair ヘルパで推論
auto q = std::make_pair(1, std::string{"hi"});
C++17CTAD
std::pair p{1, std::string{"hi"}}; // 自動推論!
std::vector v{1, 2, 3}; // std::vector<int>
std::array a{1, 2, 3}; // std::array<int, 3>
自作テンプレートでも CTAD は効く: 単純な場合はコンパイラが自動で推論してくれます。複雑な場合は「推論ガイド」を書く必要がありますが、上級テクニックなのでここでは省略。
確認クイズ
クラステンプレートを 4 問で確認。
Q1. クラステンプレートを使うとき、Box<int> b; の <int> は何?
継承関係
テンプレート引数(Box を実体化する型)
演算子オーバーロード
配列サイズ
Box の型パラメータ T に int を当てはめる指定。Box<int> は「int を格納する Box」として実体化されます。
Q2. テンプレートの実装をどこに書くべき?
.cpp に書いて .h に宣言のみ
ヘッダ側に実装まで書く
.ttl という専用ファイル
どちらでもよい
テンプレートは使用側のコンパイル時に実体化されるので、実装もヘッダ側に書きます。.cpp に書くとリンクエラー。
Q3. std::pair p{1, 2.5}; (C++17)で型は何に推論される?
std::pair<int, int>
std::pair<auto, auto>
std::pair<int, double>
コンパイルエラー
C++17 の CTAD(Class Template Argument Deduction)でコンストラクタ引数から T が推論されます。整数リテラル → int、浮動小数リテラル → double。
Q4. テンプレート特殊化の用途は?
コンパイルを速くする
特定の型だけ違う実装を提供する
テンプレートを禁止する
継承の代替
例えば「int だけ違う実装」「ポインタ型のとき違う処理」等、型ごとの特別扱いを定義できます。std::vector<bool> のビットパックが有名な(諸説ある)例。