テンプレートと concepts¶
一言で言うと¶
テンプレートは「型を引数にしたコードの雛形」。コンパイラが使用時に型ごとの実物のコードを生成(実体化)します。C#のジェネリックと似た見た目で仕組みは根本的に違い、実行時コストゼロ・制約は緩い(C++20のconceptsで改善)という特徴があります。
基本形¶
// C++20
template <typename T>
T Max(T a, T b) { return a < b ? b : a; }
Max(3, 5); // コンパイラが Max<int> を生成して呼ぶ(型は引数から推論)
Max(1.5f, 2.5f); // Max<float> も別途生成される
template <typename T, int N> // 型以外(整数など)も引数にできる
struct FixedRing {
T data[N]; // サイズがコンパイル時に決まる=スタックに置ける
// ...
};
FixedRing<int, 64> inputBuffer; // 入力履歴64フレーム分、ヒープ確保なし
仕組み: 実体化(instantiation)¶
- テンプレート自体はコードではなく雛形。
Max<int>が使われて初めて、int版の関数がその翻訳単位に生成される(→ テンプレートの実体化の内部) - だからテンプレートは通常ヘッダーに全部書く(使う側が雛形を見られる必要がある)
- 生成後はその型専用の最適なコード:
Max<int>は仮想関数もボックス化もない、手書きと同じ機械語。これが「ゼロコスト抽象化」の代表例(→ 最適化)
代償¶
- コンパイル時間: 使うたびに生成・最適化(→ ビルドが遅い理由)
- コードサイズ: 型の数だけ複製(テンプレートブロート)
- エラーメッセージ: 制約なしだと、実体化の奥深くで失敗し数百行のエラーになる(conceptsで改善)
concepts(C++20)¶
「この型はこういう操作ができること」という制約を宣言する仕組みです。
// C++20(GCC 10+/MSVC 2019+。本Wikiの検証環境GCC 9.2では未検証 — 構文はcppreference準拠)
#include <concepts>
template <typename T>
concept Damageable = requires(T& t, int amount) {
{ t.TakeDamage(amount) }; // TakeDamage(int)が呼べること
{ t.IsDead() } -> std::convertible_to<bool>;
};
template <Damageable T> // 制約付きテンプレート
void ApplyExplosion(std::vector<T>& targets, int damage) {
for (auto& t : targets)
if (!t.IsDead()) t.TakeDamage(damage);
}
// 制約を満たさない型を渡すと「DamageableじゃないB」と一行で怒られる(以前は数百行)
conceptsはダックタイピングの明文化です。仮想関数のインターフェースと違い、実行時コストなし・基底クラス不要で「この操作ができる型なら何でも」を表現します。
テンプレート vs 仮想関数(使い分けの本丸)¶
| テンプレート | 仮想関数(インターフェース) | |
|---|---|---|
| 決定時期 | コンパイル時 | 実行時 |
| 実行時コスト | ゼロ(型ごとに最適コード) | 間接呼び出し+インライン化阻害 |
| 異なる型を1つのリストに | 不可(vector |
可能(vector |
| 実行中の差し替え | 不可 | 可能 |
| コンパイル時間・コードサイズ | 増える | 増えない |
| エラー | コンパイル時(conceptsで読みやすく) | 実行時まで分からないことも |
「実行時に型が混ざる・切り替わるなら仮想関数、コンパイル時に決まるならテンプレート」が基本判断(→ 判断ガイド)。
C#のジェネリックとの違い(重要)¶
| C# ジェネリック | C++ テンプレート | |
|---|---|---|
| 実体 | 1つのILコード+実行時の型情報(参照型は共有、値型はJITが特殊化) | 型ごとにコンパイル時生成(常に特殊化) |
| 制約 | where T : IComparable(宣言必須) |
制約なしでも書ける(C++20 conceptsで任意に宣言) |
Tにできること |
制約で宣言した操作のみ | 書いてあれば何でも(実体化時に検査) |
| 数値を型引数に | 不可(C# 11以前) | 可能(FixedRing<int, 64>) |
| コンパイル | 使う側と別アセンブリでOK | 定義がヘッダーで見えている必要 |
C#で T.TakeDamage() を呼ぶには interface制約が必須ですが、C++は「実体化した型にその関数があれば通る」——強力ですが、制約がドキュメント化されない問題があり、conceptsはその明文化手段です。
Unity開発者が誤解しやすい点¶
List<T>の感覚で「テンプレートは1つのコードを共有」と思う——C++は型ごとに完全なコピーを生成。100型で使えば100個の関数ができる(ビルド時間とサイズに跳ね返る)- C#のジェネリックは実行時にも型情報が残る(
typeof(T))が、C++のテンプレートはコンパイル後は消える(実行時に「これはMaxだ」という情報はない。RTTIとは別物) - IL2CPPはC#ジェネリックを結局C++テンプレート的な特殊化に落とす(→ 第9部)——AOTでの「ジェネリック実体化漏れ」問題の背景
ゲーム開発での使用例¶
- 数学ライブラリ(
Vec<T,N>)、固定長コンテナ(リングバッファ、小容量vector) - Object Pool
<T>、ハンドル<T>などの基盤部品 - コンパイル時文字列ハッシュ、ECSの型リスト処理(上級)
- エンジンコードでの多用(STL自体、UEのTArray
等)
使う場面 / 使わない場面¶
- 使う: 複数の型で同一のロジック(コンテナ、アルゴリズム、数学)。実行時コストが許されない基盤
- 使わない: 型が1つしかない(具体型で書く)、実行時の差し替えが要る(仮想関数)、チームのC++習熟度が低い箇所での高度なメタプログラミング(可読性が死ぬ)。「テンプレートで書ける」と「書くべき」は別
よくある誤解¶
- 「テンプレートは上級者の道具で避けるべき」—
std::vector<T>を使う時点で毎日使っている。書くのは基盤部品のときだけで、使うのは日常 - 「テンプレートは遅い」— 実行は最速クラス。遅いのはコンパイル
関連項目¶
理解度チェック¶
- 「実体化」とは何がいつ起きることですか。
- C#ジェネリックとの最大の仕組みの違いは?
- conceptsが解決する2つの問題(エラーメッセージ/契約の明文化)を説明してください。
演習¶
samples/templates_basic.cpp(検証済み: 関数テンプレート+非型引数のリングバッファ)を拡張し、Clamp<T>(v, lo, hi) を追加してください。int/float両方で動くこと、sizeofの異なる型でもコードが「それぞれ生成される」ことを意識して確認してください。
前: オーバーロード | カテゴリ目次 | 次: ラムダ式・関数ポインタ・std::function