クラステンプレート | Programming Place Plus 新C++編

トップページ新C++編

先頭へ戻る

このページの概要

このページでは、クラステンプレートの基本的な部分を取り上げます。クラステンプレートは、クラスのテンプレート(雛形)を作る機能で、特定の型に依存しない汎用的なクラスを実現できます。これまで使ってきた std::vector や std::stack など、<int> のような記述が付いているものはクラステンプレートです。ここからは、こうしたクラステンプレートを自力で作る方法を理解していきましょう。

このページの解説は C++14 をベースとしています

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



リングバッファ(循環バッファ) 🔗

ペイントスクリプトをテーマにした解説は前のページまでで終わりにして、ここからは新しいテーマを使って進めていきます。次のテーマは「リングバッファ」です。

リングバッファ(循環バッファ) (ring buffer) とは、要素がリング状(円形)に並んでいるイメージのデータ構造です。先頭と末尾の要素はつながっており、要素の追加が延々と続いた場合、最初の要素のところに戻ってきて上書きされていきます。

このリングバッファを C++ らしく実装すること、そして完成したリングバッファが活用できるプログラムを作成することを目標にします。

「C++ らしく実装する」と書きましたが、つまりは C++ のコンテナ(std::vector、std::deque など)と同じような作りにするということです。たとえば、要素を追加するメンバ関数は push_back という名前で、要素の型に合わせた引数を1つ取るようにします。push や add のような名前を選ぶと、標準のコンテナの作り方から外れていることになります。

std::vector<int> int_vector {};
std::vector<std::string> str_vector {};

// 同じかたちで変数を定義できる
RingBuffer<int> int_ringbuf {};
RingBuffer<std::string> str_ringbuf {};

// 同じような使い方ができる
int_vector.push_back(123);
int_ringbuf.push_back(123);
for (auto v : int_vector) {
    // ...
}
for (auto v : int_ringbuf) {
    // ...
}

標準コンテナの作りに合わせておくと、ほかのだれかが使うときの学習コストが低下しますし、別のコンテナを使ったほうが適切だと気付いたとき、差し替えやすくもなります。また、必要になる場面はあまりないですが、コンテナアダプタが使う内部コンテナとして利用できるかもしれません(「queue、priority_queue、deque」のページを参照)。

クラステンプレート 🔗

リングバッファを実装した RingBufferクラスを作るにあたって、現状で不足している知識は、任意の型の要素を取り扱う方法です。std::vector ではいつも std::vector<int> のようにして、要素の型を指定していましたが、これはどうやって実現しているのでしょう。

この答えが、このページで解説するクラステンプレート (class template) です。テンプレートクラス (template class) と呼ぶこともあります。

クラステンプレートは、その名のとおり「クラスの雛形 (ひながた)」です。基本的な形式は決められていて、空欄のところを埋めてくれという文書や書類をテンプレ(=テンプレート)と呼ぶことがありますが、まさにそのイメージです。クラステンプレートは、クラスの実装の大枠は固定されているが、部分的に空欄にしてあって、クラステンプレートを使うときにその空欄部分を埋めて使うという仕組みです。

クラスと構造体に本質的な違いはないので、構造体に対しても同じ使い方ができます(「クラス」のページを参照)。

クラステンプレートを定義する 🔗

クラステンプレートは次のように定義します。classstruct にもできます。違いはクラスのところで解説したとおりです(「クラス」のページを参照)。

template <テンプレート仮引数の宣言の並び>
class クラステンプレート名 {
    メンバの宣言;
      :
};

通常のクラス定義の手前に、templateキーワード と「<テンプレート仮引数の宣言の並び>」を置くことでクラステンプレートの定義になります。

クラステンプレートは、名前空間スコープ(グローバルスコープも含む)、クラススコープの中でのみ定義できます[1](「スコープと名前空間」のページを参照)。たとえば、関数内にローカルなクラステンプレートは定義できません。

「テンプレート仮引数の宣言の並び」には1つ以上のテンプレート仮引数(テンプレートパラメータ) (template parameter) を記述します。2つ以上あるときは , で区切ります。1つ分のテンプレート仮引数は次のいずれかの方法で記述します。

typename 名前
class 名前

typenameキーワード と classキーワードはどちらを使ってもまったく同じです。ここでは struct は使えません。「名前」がテンプレート仮引数の名前です。あまりないことですが、名前をどこにも使わないのなら指定を省略できます。

template <typename T1, typename T2>
class C {
    // ...
};

// 上とまったく同じ意味
template <class T1, class T2>
class C {
    // ...
};

テンプレート仮引数の名前は、クラステンプレートの実装の中でのみ使用できます。

template <typename T>
class C {
public:
    C(T a, T b, T c);

    inline T get() const
    {
        return m_value;
    }

private:
    T  m_value;
};

template <typename T>
C<T>::C(T a, T b, T c) :
    m_value {a + b + c}
{
}

テンプレート仮引数 T を宣言しており、いたるところで T を使っています。メンバ関数をクラステンプレート定義の外側に書くときの記述はかなり面倒ですが、template <テンプレート仮引数の宣言の並び> をもう1度同じように書いたうえ、メンバ関数の名前の手前に クラステンプレート名<テンプレート仮引数の名前の並び>:: を書きます。

静的データメンバの定義をクラステンプレート定義の外側に書く場合も同様です。

template <typename T>
class C {
    // ...

    static int ms_value;
};


template <typename T>
int C<T>::ms_value {100};

通常のクラスとは異なり、クラステンプレートのメンバ関数を使用する側から、そのメンバ関数の定義が見えている必要があります。メンバ関数の定義をクラステンプレート定義の外側に記述する場合も、そのメンバ関数の定義が、呼び出し側から見えるところになければなりません。通常のクラスなら、メンバ関数の宣言はヘッダファイルに、定義はソースファイルに分けて書けますが、クラステンプレートではうまくいきません。そのため、メンバ関数の定義も、クラステンプレートの定義と同じヘッダファイルに書きます。

どうしてもメンバ関数の定義を別ファイルに分けたい場合は、別のヘッダファイルを作ってそちらにメンバ関数の定義だけを置く方法を取ることもあります。結果的に呼び出し側から見えるところに定義があれば大丈夫です。

テンプレートのインスタンス化 🔗

クラステンプレートの実装のなかで、テンプレート仮引数の名前が使われている箇所が、前述した「テンプレの空欄部分」です。空欄部分が残っているということは、クラステンプレートはクラスとしては不完全であり、クラスとして使える状態ではありません。したがって、クラステンプレートはクラスではありません

クラステンプレートをクラスとして使うためには、空欄部分を埋める情報を与えてやる必要があります。この情報はテンプレート実引数 (template argument) によって指定できます。std::vector を使うときにいつもやっていることがそれに当たります。

std::vector<int> int_vec {};  // テンプレート実引数として int を指定

つまり、クラステンプレート名のうしろに <テンプレート実引数の並び> を与えてやります。もちろん、テンプレート実引数の並びは、テンプレート仮引数の並びに対応します。テンプレート仮引数の宣言順のとおりに、テンプレート実引数を指定します。

なお、std::vector<int> のような、テンプレートの名前とテンプレート実引数を並べた記述をテンプレートID (template-id) と呼びます。

後述するデフォルトテンプレート実引数があるので、<> の内側が省略される場合があります。

さきほどのクラステンプレート C のテンプレート仮引数 T に int を与えたとすると、クラステンプレート内で空欄 T だった部分はすべて int に置き換えられます。つまり、コンパイラが次のようなクラス定義を生成します。

class C {
public:
    C(int a, int b, int c);

    inline int get() const
    {
        return m_value;
    }

private:
    int  m_value;
};

C::C(int a, int b, int c) :
    m_value {a + b + c}
{
}

これでもう空欄部分は残されておらず、クラスとして正しい定義になっており、実際にクラスとして使用できる状態です。この時点でクラスとして正しくない部分があるようならコンパイルエラーになります。

このように、テンプレート実引数を与えることによって、利用可能なコード(実体)が生成されることを、テンプレートのインスタンス化(実体化、具現化) (template instantiation) と呼びます。また、生成された実体のことを、(日本語だとちょっと違和感があるかもしれませんが)(テンプレートの)特殊化 (specialization) と呼びます。

【上級】ここで取り上げたような特殊化は暗黙的特殊化 (implicit specialization) と呼ばれます。このほかに、専用の構文を使ってプログラマーが指示を与えて行う明示的特殊化 (explicit specialization) があります[2]。特殊化についての詳細は、ページを改めて取り上げます。

テンプレート実引数には int以外も指定できますから、1つのクラステンプレートを定義しただけで、一気にさまざまな型に対応されたことになります。将来新しく定義される型にすらも、すでに対応力を持っていることになります。

C<int> ic(1, 2, 3);                  // OK。T が int で埋められる
C<double> dc(1.0, 1.5, 2.0);         // OK。T が double で埋められる

// 将来、X という型が作られても、クラステンプレートを修正することなく使える可能性がある
// C<X> xc(x1, x2, x3);

テンプレート仮引数 T を int で置き換えて作られるクラスと、double に置き換えて作られるクラスは当然異なるものですから、C<int>C<double> は異なる型です。互いに型変換することはできません。また、異なるクラスとして、それぞれのコードが生成されるので、その分だけプログラムサイズ(実行ファイルのサイズ)が大きくなることは注意しなければなりません。

【上級】互いに型を変換し合えたほうがいい場合は、テンプレート変換コンストラクタ(「関数テンプレート」のページを参照)やテンプレート変換演算子を実装します。

関数の引数では、実引数に指定した「値」を仮引数へ渡しますが、テンプレートの引数は、テンプレート実引数に指定した「型」をテンプレート仮引数へ渡しているといえます。このようなプログラミングの方法をジェネリックプログラミング (generic programming) と呼び、特定の型に依存しない(特定の型に限定されない)プログラムが実現できます。

ただし、ノンタイプテンプレート(非型テンプレート)といって、テンプレート実引数に型ではなく定数式を指定する機能も存在します。

リングバッファの実装 🔗

では、リングバッファをクラステンプレートとして実装してみます。リングバッファそのものの詳しい解説は省略します。必要であれば、アルゴリズムとデータ構造編>「キュー」を参照してください。

ここではいきなりクラステンプレートとして作成しますが、慣れないうち、あるいは複雑な場合は、いったん型を int などで固定した普通のクラスを作ってから、クラステンプレートに書き換えるほうが作りやすいかもしれません。

// ring_buffer.h
#ifndef RING_BUFFER_H_INCLUDED
#define RING_BUFFER_H_INCLUDED

#include <cassert>
#include <vector>

namespace mylib {

    // リングバッファ
    template <typename T>
    class RingBuffer {
    public:
        using value_type = T;                           // 要素型
        using reference = value_type&;                  // 要素の参照型
        using const_reference = const value_type&;      // 要素の const参照型
        using pointer = value_type*;                    // 要素のポインタ型
        using const_pointer = const value_type*;        // 要素の constポインタ型
        using size_type = std::size_t;                  // サイズ型

    public:
        // コンストラクタ
        //
        // size: 容量
        explicit RingBuffer(size_type capacity);


        // 要素を追加
        void push_back(const value_type& value);

        // 要素を取り除く
        void pop_front();

        // 空にする
        void clear();


        // 先頭の要素の参照を返す
        inline reference front()
        {
            assert(empty() == false);
            return m_data[m_front];
        }

        // 先頭の要素の参照を返す
        inline const_reference front() const
        {
            assert(empty() == false);
            return m_data[m_front];
        }

        // 末尾の要素の参照を返す
        inline reference back()
        {
            assert(empty() == false);
            return m_data[get_prev_pos(m_back)];
        }

        // 末尾の要素の参照を返す
        inline const_reference back() const
        {
            assert(empty() == false);
            return m_data[get_prev_pos(m_back)];
        }



        // 容量を返す
        inline size_type capacity() const
        {
            return m_data.size();
        }

        // 要素数を返す
        inline size_type size() const
        {
            return m_size;
        }

        // 空かどうかを返す
        inline bool empty() const
        {
            return m_size == 0;
        }

        // 満杯かどうかを返す
        inline bool full() const
        {
            return m_size == capacity();
        }

    private:
        // 次の位置を返す
        inline size_type get_next_pos(size_type pos) const
        {
            return (pos + 1) % capacity();
        }

        // 手前の位置を返す
        inline size_type get_prev_pos(size_type pos) const
        {
            if (pos >= 1) {
                return pos - 1;
            }
            else {
                return m_size - 1;
            }
        }

    private:
        std::vector<value_type>     m_data;         // 要素
        size_type                   m_size {0};     // 有効な要素の個数
        size_type                   m_back {0};     // 次に push される位置
        size_type                   m_front {0};    // 次に pop される位置
    };


    // コンストラクタ
    template <typename T>
    RingBuffer<T>::RingBuffer(size_type capacity) : m_data(capacity)
    {
    }

    // 要素を追加
    template <typename T>
    void RingBuffer<T>::push_back(const value_type& value)
    {
        if (full()) {
            m_front = get_next_pos(m_front);
        }
        else {
            m_size++;
        }

        m_data[m_back] = value;
        m_back = get_next_pos(m_back);
    }

    // 要素を取り除く
    template <typename T>
    void RingBuffer<T>::pop_front()
    {
        assert(empty() == false);

        m_front = get_next_pos(m_front);
        --m_size;
    }

    // 空にする
    template <typename T>
    void RingBuffer<T>::clear()
    {
        m_size = 0;
        m_back = 0;
        m_front = 0;
    }

}

#endif
// main.cpp
#include <iostream>
#include "ring_buffer.h"

int main()
{
    // テスト。容量 8 のリングバッファを作成
    mylib::RingBuffer<int> rb {8};

    // 初期状態が正常か確認
    assert(rb.size() == 0);
    assert(rb.capacity() == 8);
    assert(rb.empty());
    assert(rb.full() == false);

    // 容量を超えて要素を追加
    for (int i = 0; i < 10; ++i) {
        rb.push_back(i);
        std::cout << "size = " << rb.size() << "\n";
    }

    // 空になるまで要素を取り出し続ける
    while (rb.empty() == false) {
        auto v = rb.front();
        rb.pop_front();
        std::cout << "v = " << v << "\n";
    }
}

実行結果:

size = 1
size = 2
size = 3
size = 4
size = 5
size = 6
size = 7
size = 8
size = 8
size = 8
v = 2
v = 3
v = 4
v = 5
v = 6
v = 7
v = 8
v = 9

コンストラクタの引数で容量(格納できる要素の最大数)を指定するようにしています。この指定を受けて要素を管理する配列を作りたいので、内部的には std::vector を使って要素を管理します。要素の型は RingBuffer のテンプレート実引数によって決まるものですから、テンプレート仮引数の T ということになりますが、ここでは std::vector<T> ではなく std::vector<value_type> としています。

value_type、reference などの typedef名は、std::vector に合わせて定義したものです。テンプレート仮引数の名前はそのクラステンプレートの実装内でしか使えないため、クラステンプレートの使用者側からも使いたいとなれば、このように typedef名を定義して公開しなければなりません。

mylib::RingBuffer<int> rb {8};

T& r = rb.front();  // T はクラステンプレートの外側で使えない

// ほかの関数に渡すような場合も困る
void f(T& r)  // エラー
{
    // ...
}
f(rb.front());

もちろん、 rbmylib::RingBuffer<int> と宣言したことをプログラマーは分かっているので、Tint と書くことは可能ですし、変数に受け取るときには auto にするのがより簡単ではありますが、型を明示的に書きたい・書かなければならない場合には困ります。また、int と書いてしまうとあとから型を変更するようなときに事故の元です。reference という typedef名が public に提供されていることによって、呼び出し側でもこの名前を使えます。

mylib::RingBuffer<int> rb {8};

mylib::RingBuffer<int>::reference r = rb.front();  // OK

void f(mylib::RingBuffer<int>::reference r)  // OK
{
    // ...
}
f(rb.front());

依存名と typenameキーワード 🔗

さきほどの RingBuffer クラステンプレートで、メンバ関数の定義をクラステンプレートの定義の外側に書くようにするとしたら、戻り値の型のところに RingBuffer<T>::reference のような記述が出現することになります。これは型名であるはずですが、コンパイラはそのようには解釈しません。

template <typename T>
RingBuffer<T>::reference RingBuffer<T>::front()  // エラー
{
    assert(empty() == false);
    return m_data[m_front];
}

RingBuffer<T>::referenceT::referenceC<T>::X のように、テンプレート仮引数が絡んでいる名前のことを依存名 (dependent name) といいますが、依存名は、一部の例外的な場面を除いて、型の名前ではないものとして扱われるルールがあるためです[3]。このコードの場合、戻り値の「型」を記述しなければならない場所に、依存名である RingBuffer<T>::reference が現れたためコンパイルエラーになります。

このようなときには、依存名の直前に typenameキーワードを置くことで、型名であることを明示します。

template <typename T>
typename RingBuffer<T>::reference RingBuffer<T>::front()  // OK
{
    assert(empty() == false);
    return m_data[m_front];
}

現在の RingBufferクラステンプレートの実装では、要素を管理するコンテナを std::vector<value_type> としています。コンテナを変更しやすく余地を作っておくために、typedef名を定義してもいいでしょう。

template <typename T>
class RingBuffer {
public:
    using container_type = typename std::vector<T>;                     // 内部コンテナの型

private:
    container_type              m_data;         // 要素
};

また、value_typeT の別名になっていますが、value_type は要素の型のことなので、実際に要素を管理している container_type の要素の型の別名であるほうが適切といえます。標準コンテナには、要素の型をあらわす typedef名として、やはり value_type が提供されているので、container_type::value_type の別名としてやるといいでしょう。container_type が依存名なので、container_type::value_type も依存名です。したがってここでも typenameキーワードによる明示が必要です。

using value_type = typename container_type::value_type;

同じようにほかの typedef名も置き換えて、RingBuffer クラステンプレートは次のように修正されます。

// ring_buffer.h
#ifndef RING_BUFFER_H_INCLUDED
#define RING_BUFFER_H_INCLUDED

#include <cassert>
#include <vector>

namespace mylib {

    // リングバッファ
    template <typename T>
    class RingBuffer {
    public:
        using container_type = typename std::vector<T>;                     // 内部コンテナの型
        using value_type = typename container_type::value_type;             // 要素型
        using reference = typename container_type::reference;               // 要素の参照型
        using const_reference = typename container_type::const_reference;   // 要素の const参照型
        using pointer = typename container_type::pointer;                   // 要素のポインタ型
        using const_pointer = typename container_type::const_pointer;       // 要素の constポインタ型
        using size_type = typename container_type::size_type;               // サイズ型

    public:
        // コンストラクタ
        //
        // size: 容量
        explicit RingBuffer(size_type capacity);


        // 要素を追加
        void push_back(const value_type& value);

        // 要素を取り除く
        void pop_front();

        // 空にする
        void clear();


        // 先頭の要素の参照を返す
        inline reference front()
        {
            assert(empty() == false);
            return m_data[m_front];
        }

        // 先頭の要素の参照を返す
        inline const_reference front() const
        {
            assert(empty() == false);
            return m_data[m_front];
        }

        // 末尾の要素の参照を返す
        inline reference back()
        {
            assert(empty() == false);
            return m_data[get_prev_pos(m_back)];
        }

        // 末尾の要素の参照を返す
        inline const_reference back() const
        {
            assert(empty() == false);
            return m_data[get_prev_pos(m_back)];
        }



        // 容量を返す
        inline size_type capacity() const
        {
            return m_data.size();
        }

        // 要素数を返す
        inline size_type size() const
        {
            return m_size;
        }

        // 空かどうかを返す
        inline bool empty() const
        {
            return m_size == 0;
        }

        // 満杯かどうかを返す
        inline bool full() const
        {
            return m_size == capacity();
        }

    private:
        // 次の位置を返す
        inline size_type get_next_pos(size_type pos) const
        {
            return (pos + 1) % capacity();
        }

        // 手前の位置を返す
        inline size_type get_prev_pos(size_type pos) const
        {
            if (pos >= 1) {
                return pos - 1;
            }
            else {
                return m_size - 1;
            }
        }

    private:
        container_type              m_data;         // 要素
        size_type                   m_size {0};     // 有効な要素の個数
        size_type                   m_back {0};     // 次に push される位置
        size_type                   m_front {0};    // 次に pop される位置
    };


    // コンストラクタ
    template <typename T>
    RingBuffer<T>::RingBuffer(size_type capacity) : m_data(capacity)
    {
    }

    // 要素を追加
    template <typename T>
    void RingBuffer<T>::push_back(const value_type& value)
    {
        if (full()) {
            m_front = get_next_pos(m_front);
        }
        else {
            m_size++;
        }

        m_data[m_back] = value;
        m_back = get_next_pos(m_back);
    }

    // 要素を取り除く
    template <typename T>
    void RingBuffer<T>::pop_front()
    {
        assert(empty() == false);

        m_front = get_next_pos(m_front);
        --m_size;
    }

    // 空にする
    template <typename T>
    void RingBuffer<T>::clear()
    {
        m_size = 0;
        m_back = 0;
        m_front = 0;
    }

}

#endif

【C++20】構文のルールからいって、型以外が現れるはずがない箇所については typename を置かなくても型名であると判断されるようになりました(C++20 より前の時点でも、一部の場面では同様の判断が行われていました)。メンバ関数の戻り値の型や、型の別名の元になる型名についても typename は不要になっています。[4] [5]

デフォルトテンプレート実引数 🔗

関数にデフォルト実引数を指定できるように(「コンストラクタ」のページを参照)、テンプレートにはデフォルトテンプレート実引数 (default template augument) を指定できます。テンプレート実引数を指定しなかったときに、デフォルトの型が指定されます。

デフォルトの型は、テンプレート仮引数を記述するときに = と型名を続けて書くことで指定できます。

template <テンプレート仮引数 =>

先に定義されたテンプレート仮引数を、後続の「型」に使うこともできます。

template <typename T1 = int, typename T2 = T1>

デフォルトテンプレート実引数があるテンプレート仮引数のうしろに、デフォルトテンプレート実引数がないテンプレート仮引数が続くことは許されません[10]

// T2 にデフォルトテンプレート実引数があるので、T3 にも必要
template <typename T1, typename T2 = double, typename T3>

【上級】この制約はクラステンプレートとエイリアステンプレートに対してしか課せられておらず、関数テンプレートであれば問題ありません。ただし、その関数テンプレートを使うときに、すべてのテンプレート実引数が決定できる必要があります。

次のコードは使用例です。

template <typename T = int> // T のデフォルトは int
class C {
public:
    C(T a, T b, T c);

    inline T get() const
    {
        return m_value;
    }

private:
    T  m_value;
};

template <typename T>   // こちらにはデフォルト実引数の記述は不要
C<T>::C(T a, T b, T c) :
    m_value {a + b + c}
{
}

int main()
{
    C<> c1(1, 2, 3);     // OK. T は int
    C<int> c2(1, 2, 3);  // OK
}

メンバ関数の定義をクラステンプレート定義の外側に記述するとき、デフォルトテンプレート実引数の指定は不要です[6]

クラステンプレートをインスタンス化するとき、明示的に記述するテンプレート実引数がない場合でも、<> 自体は必要です。

【C++17】クラステンプレートのコンストラクタの実引数を使って、テンプレート実引数を推論する機能が加わったため、<> 自体を省略できる場合もあります[7]

【C++20】クラステンプレートを集成体初期化する場合、その内容からテンプレート実引数が推論できるようになりました[8]

テンプレート実引数の指定を省略できるのは、末尾に近い引数に対してだけです。

template <typename T1 = int, typename T2 = double, typename T3 = double>
class C {
    // ...
};

// 先頭や途中のテンプレート実引数を省略することはできない
C<, int, int> c1(1, 2, 3);
C<int, , int> c2(1, 2, 3);

また、テンプレート実引数の並びに余分な , が残ってしまう記述は許されません。

template <typename T1 = int, typename T2 = double, typename T3 = double>
class C {
    // ...
};

C<int, int, > c3(1, 2, 3);

エイリアステンプレート 🔗

エイリアス宣言(「符号無し整数」のページを参照)を用いてテンプレートを宣言すると、エイリアステンプレート (alias template) になります。エイリアステンプレートによってクラステンプレートの別名を作れます。

template <テンプレート仮引数の宣言の並び>
using エイリアステンプレートの名前 = 元のクラステンプレートの名前<テンプレート仮引数の並び>;

型の別名を作るには typedef指定子を使う方法もありますが(「符号無し整数」のページを参照)、エイリアステンプレートはエイリアス宣言を使わなければ実現できません。


次のコードは、RingBufferクラステンプレートのエイリアステンプレートである CircularBuf を定義したことになります。

template <typename T>
using CircularBuf = RingBuffer<T>;

この時点で CircularBuf はあくまでもテンプレート(エイリアステンプレート)です。エイリアステンプレートをインスタンス化することで、元になったクラステンプレートをインスタンス化するのと同じ結果を得られます。

CircularBuf<int> cir_buffer {};
RingBuffer<int> ring_buffer {};

この場合、CircularBuf<int>RingBuffer<int> はまったく同一の型です。

エイリアステンプレートを使うと、複数あるテンプレート実引数の一部だけを指定したテンプレートを作ることができます。

template <typename T1, typename T2>
class C {};

template <typename T>
using Alias = C<T, int>;  // 第2テンプレート実引数を int に強制した C の別名

Alias<int> a1 {};     // C<int, int> と同等の型
Alias<double> a2 {};  // C<double, int> と同等の型

クラステンプレート C には2つのテンプレート仮引数がありますが、エイリアステンプレート Alias を定義するときに、2つ目のほうに int を与えています。エイリアステンプレートにはテンプレート仮引数 T があるので、Alias のほうでテンプレートをインスタンス化するときには、T に対するテンプレート実引数だけを指定します。


なお、クラステンプレートを特定の型でインスタンス化したあとの型に対して別名を付けることは typedef でも using でも可能です。インスタンス化済みということは、すでに完全なクラスになっているためです。

template <typename T1, typename T2>
class C {};

// T1、T2 を int にしてインスタンス化したあとのクラス型に対する別名を定義。
// 以下のどちらでも同じ。
typedef C<int, int> IntC;
using IntC = C<int, int>;

std::string、std::ifstream など、これまでのページに登場した標準ライブラリのいくつかのクラスたちは、これと同じ方法を使って定義された名前です。次のように、クラステンプレートをインスタンス化したあとの別名として定義されています。

namespace std {
    using string = basic_string<char>;
    using ifstream  = basic_ifstream<char>;
    using ofstream  = basic_ofstream<char>;
    using fstream  = basic_fstream<char>;
}

【上級】char の部分をほかの文字型に置き換えることが可能というわけで、同じ実装が異なる文字型に対しても動作するように設計されています。また、char 以外の文字型を使う場合に対応した別名も定義されています。

テンプレートテンプレート仮引数(テンプレートテンプレートパラメータ) 🔗

次のようなクラステンプレートを作ろうとしているとします。

template <typename T>
class C {
public:
    // ...

private:
    struct Data {
        T a;
        int b;
        int c;
    };

    std::vector<Data>   m_data;
};

ここで、データを管理するコンテナを std::deque に取り換えられるようにしたいとします。重要なのは、要素の型は C::Data でなければならないという点です。つまり、std::vector<Data> から std::deque<Data> にすることを望んでいます。次のようにテンプレート仮引数 Container を追加すればできるように思うかもしれませんが、これはエラーになってしまいます。

template <typename T, typename Container>
class C {
public:
    // ...

private:
    struct Data {
        T a;
        int b;
        int c;
    };

    Container<Data> m_data;  // エラー。Container にテンプレート実引数は指定できない
};

C<int, std::deque> c {};  // エラー。テンプレート実引数の指定がない std::deque を渡せない

ひとことでいえば、テンプレート仮引数に対して、クラステンプレートを渡すことはできないということです。そのため、Container<Data>C<int, std::deque> も許されません。

また、std::deque に対するテンプレート実引数の指定がありませんが、C<int, std::deque<C::Data>> とすることもできません。C::Data は private なのでアクセスできませんし、できたとしてもこれでは、Container<C::Data><Data> m_data; となってしまいそうです。

そこで、テンプレートテンプレート仮引数(テンプレートテンプレートパラメータ) (template template parameter) を使います。テンプレートテンプレート仮引数は、テンプレート引数にクラステンプレートを使えるようにする機能です。文法は次のようになります。

template <template <typename テンプレート仮引数の名前> class テンプレートテンプレート仮引数の名前>
class {
    // ...
};

または

template <template <class テンプレート仮引数の名前> class テンプレートテンプレート仮引数の名前>
class {
    // ...
};

複雑なのは、テンプレート仮引数として使えるようにしたいクラステンプレートのテンプレート仮引数を記述しなければならないためです。内側にある template <typename テンプレート仮引数の名前> class テンプレートテンプレート仮引数の名前 の部分がこれに当たります。

テンプレート仮引数を記述する際の typenameclass にしても同じ意味になるのは前に取り上げたとおりです。しかし、テンプレートテンプレート仮引数の名前を記述するときに使う class はどういうわけか class でなければなりません。

【C++17】この不自然な制約は撤廃され、typename を使ってもいいことになりました[9]

テンプレートテンプレート仮引数を使うと、次のように書けます。

template <typename T, template <typename T, typename Allocator = std::allocator<T>> class Container>
class C {
public:
    // ...

private:
    struct Data {
        T a;
        int b;
        int c;
    };

    Container<Data> m_data;
};

int main()
{
    C<int, std::deque> c {};
}

テンプレートテンプレート仮引数と直接的に関係する話ではないですが、std::vector や std::deque などを使えるようにするためには、

template <typename T, template <typename T> class Container>

ではなく、次のように書かなけれなりません。

template <typename T, template <typename T, typename Allocator = std::allocator<T>> class Container>

これは、std::vector や std::deque には以下のようにデフォルトテンプレート実引数があるためです。

template <typename T, typename Allocator = std::allocator<T>> class vector;
template <typename T, typename Allocator = std::allocator<T>> class deque;

デフォルトテンプレート実引数があっても、そこにテンプレート仮引数が存在するのは事実なので、きちんと書き出してやらなければなりません。

まとめ 🔗


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク 🔗


練習問題 🔗

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (確認★)

次の用語について説明してください。

解答・解説

問題2 (基本★)

2次元の座標を表現する Point クラステンプレートを作成してください。座標を任意の型で表現できるようにしてください。

解答・解説

問題3 (応用★★★)

std::stack(「再帰呼び出しとスタック」のページを参照)の作りを参考にして、スタックのクラステンプレートを作成してください。

解答・解説


解答・解説ページの先頭



更新履歴 🔗




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る