【C++】テンプレートとは?|汎用的なクラスや関数を作る基本

【C++】テンプレートとは?汎用的なクラスや関数を作る基本のアイキャッチ画像 C++

今回は、C++でよく出てくる「テンプレートとは何か」について解説していきます。

「テンプレートって何?」
「std::vector の < > の中身って何を書いているの?」
「同じ処理を型ごとに何回も書くのが面倒…」

こんな疑問はありませんか?

普段なにげなく使っている std::vector<int>std::vector<Bullet> は、実はテンプレートの仕組みで作られています。テンプレートを理解すると、標準ライブラリの裏側が見えてきて、自分でも「どんな型でも使える」便利なクラスや関数を作れるようになります。

この記事を読み終えると、あなたはテンプレートの意味・書き方・ゲームでの使いどころ・初心者がハマりやすいミスをしっかり理解できると思いますので、ぜひ最後まで読んでいただけると嬉しいです。

テンプレートとは?

テンプレートとは、型をあとから差し込めるようにする仕組みです。いわば「型の設計図」で、1つのコードを int でも double でも自作の型でも使い回せるようになります。

実は std::vector もテンプレートです。std::vector<int>std::vector<Bullet> は、同じ vector というテンプレートに違う型を差し込んで作られた別物、というわけです。vector自体の使い方は配列とvectorの違いの記事でも解説しています。

なぜテンプレートが必要?

テンプレートが必要な理由は、型ごとに同じコードを何度も書かなくて済むからです。

もしテンプレートが無いと、「intを比べる関数」「doubleを比べる関数」…と、中身がほとんど同じ関数を型の数だけ量産することになります。テンプレートを使えば共通の処理を1回書くだけで済み、修正も1か所で終わります。

関数テンプレートの基本

まずは関数から。template <typename T> と書くと、T が「あとで決まる型」になります。

#include <iostream>

// T は「あとで決まる型」
template <typename T>
T getMax(T a, T b) {
    return (a > b) ? a : b;
}

int main() {
    std::cout << getMax(3, 8) << "n";      // int として使う
    std::cout << getMax(2.5, 1.2) << "n";  // double として使う
    return 0;
}
8
2.5

getMax(3, 8) と呼ぶと T は自動的に int に、getMax(2.5, 1.2) なら double になります。呼び出し方から型を判断してくれるので、こちらが型を書く必要はありません。

実践例:ゲームのオブジェクトプール

クラスにもテンプレートは使えます(クラスの基本はこちら)。ゲームでよくあるオブジェクトプールを例にすると、テンプレートの便利さが一気に伝わります。

オブジェクトプールとは、オブジェクトを毎回 new / delete せず、あらかじめ用意して使い回す仕組みです。弾やエフェクトのように大量に出したり消したりするものは、これで動作が軽くなります。テンプレートにしておけば、弾でも敵でもエフェクトでも同じプールを使い回せます

#include <iostream>
#include <vector>

// どんな型でも使い回せるオブジェクトプール
template <typename T>
class ObjectPool {
public:
    ObjectPool(int size) {
        pool.resize(size);
        used.resize(size, false);
    }

    // 空いているオブジェクトを1つ貸し出す
    T* Acquire() {
        for (int i = 0; i < (int)pool.size(); i++) {
            if (!used[i]) {
                used[i] = true;
                return &pool[i];
            }
        }
        return nullptr; // 空きなし
    }

    // 使い終わったオブジェクトを返却する
    void Release(T* obj) {
        for (int i = 0; i < (int)pool.size(); i++) {
            if (&pool[i] == obj) {
                used[i] = false;
                return;
            }
        }
    }

private:
    std::vector<T> pool;
    std::vector<bool> used;
};

struct Bullet {
    float x = 0.0f;
    float y = 0.0f;
};

int main() {
    ObjectPool<Bullet> bullets(3);

    Bullet* b = bullets.Acquire();
    b->x = 100.0f;
    std::cout << "弾を確保: x = " << b->x << "n";

    bullets.Release(b);
    std::cout << "弾を返却しましたn";
    return 0;
}
弾を確保: x = 100
弾を返却しました

ObjectPool<Bullet> と書けば弾用、ObjectPool<Enemy> と書けば敵用のプールになります。オブジェクトの初期化と後始末の考え方はコンストラクタとデストラクタの記事もあわせて読むと理解が深まります。

【重要】私が実際にテンプレートで困った体験談

個人開発でこのオブジェクトプールをテンプレートで作ったとき、いつもの癖で実装を .cpp に分けて書いたところ、ビルドした瞬間に大量のリンクエラーが出て数時間ハマりました。

error LNK2019: 未解決の外部シンボル "public: struct Bullet * __thiscall
ObjectPool<struct Bullet>::Acquire(void)" が関数 _main で参照されました。

原因は、テンプレートは「使う型が決まって初めて実体のコードが作られる」という性質にありました。実装を .cpp に隠してしまうと、リンカがその実体を見つけられずエラーになるのです。

解決策はシンプルで、テンプレートの実装はヘッダ(.h)に書くことでした。次のように、宣言と実装をまとめてヘッダに置けば一発で通ります。

▼ ObjectPool.h

#pragma once
#include <vector>

template <typename T>
class ObjectPool {
public:
    // 宣言と実装をヘッダにまとめて書くのがポイント
    T* Acquire() { /* ... */ return nullptr; }
};

テンプレート使用時のよくある失敗例と対処法

初心者がつまずきやすいポイントを3つ紹介します。

①実装を.cppに書いてリンクエラー(LNK2019)

体験談のとおりです。テンプレートは実装もヘッダに書くのが基本、と覚えておけば回避できます。

②渡した型が必要な操作を持っていない

たとえば getMax は内部で > を使うので、比較できない型を渡すとコンパイルエラーになります。テンプレートが要求する操作(演算子など)を、渡す型が持っているかを確認しましょう。エラーメッセージはとても長いですが、まずは最初の数行だけ読むのがコツです。

③何でもテンプレートにして複雑化する

便利だからと全部テンプレートにすると、コンパイル時間が伸びたりエラーが読みにくくなったりします。「本当に複数の型で使い回すもの」だけテンプレートにするのがおすすめです。

注意点

  • テンプレートの実装はヘッダ(.h)に書く
  • エラーメッセージは長くなりがち。最初の数行から原因を探る
  • 渡す型が、テンプレート内で使う操作を持っている前提で設計する

まとめ

  • テンプレートは型をあとから差し込める仕組み(型の設計図)
  • std::vector をはじめ、標準ライブラリの多くはテンプレートで作られている
  • ゲームのオブジェクトプールのように、同じ仕組みを色々な型で使い回せる
  • 実装はヘッダに書く。これを外すとリンクエラーにハマりやすい

テンプレートは最初こそ記号が多くて難しく見えますが、「型を後で決められる便利な設計図」と捉えると一気に身近になります。まずは std::vector の裏側をイメージしながら、簡単な関数テンプレートから試してみてください。

関連記事