C++ Learning

第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> のビットパックが有名な(諸説ある)例。