クラステンプレート | Programming Place Plus C++編【言語解説】 第20章

トップページC++編

C++編で扱っている C++ は 2003年に登場した C++03 という、とても古いバージョンのものです。C++ はその後、C++11 -> C++14 -> C++17 -> C++20 と更新されており、今後も 3年ごとに更新されます。
なかでも C++11 での更新は非常に大きなものであり、これから C++ の学習を始めるのなら、C++11 よりも古いバージョンを対象にするべきではありません。特に事情がないなら、新しい C++ を学んでください。 当サイトでは、C++14 をベースにした新C++編を作成中です。

この章の概要

この章の概要です。


クラステンプレート

第9章で、関数をテンプレート化できることを解説しましたが、本章では、クラスをテンプレート化してみます。

クラスの場合は、クラステンプレートと呼ばれます。また、class と struct は概念としては同等なので(第12章)、構造体もテンプレート化できます。

クラステンプレートでなく、テンプレートクラスと表現することもあります。同じ意味で使われていますが、まれに、クラステンプレートに具体的な型を当てはめて作り出される具体的なクラスのことを指して、テンプレートクラスと呼んでいることがあります。紛らわしいので、クラステンプレートと表現する方が良いと思われます。

クラステンプレートを使うと、メンバ変数やメンバ関数の引数・戻り値の型を任意に指定できます。そのため、たとえば、int型でも double型でも std::string型でも、さらにはユーザが任意で作ったクラス型でも扱えるようなスタッククラスを作り出せます。

template <typename T>
class Stack {
public:
    explicit Stack(std::size_t capacity);
    ~Stack();

    void Push(const T& data);
    void Pop();
    inline const T Top() const
    {
        return mData[mSP - 1];
    }

    inline std::size_t GetSize() const
    {
        return mSP;
    }
    inline std::size_t GetCapacity() const
    {
        return mCapacity;
    }

private:
    const std::size_t      mCapacity;
    T*                     mData;
    int                    mSP;
};

template <typename T>
Stack<T>::Stack(std::size_t capacity) :
    mCapacity(capacity),
    mData(new T[capacity]),
    mSP(0)
{
}

template <typename T>
Stack<T>::~Stack()
{
    delete [] mData;
}

template <typename T>
void Stack<T>::Push(const T& data)
{
    assert(static_cast<std::size_t>(mSP) < mCapacity);
    mData[mSP] = data;
    mSP++;
}

template <typename T>
void Stack<T>::Pop()
{
    assert(mSP > 0);
    mSP--;
}

クラステンプレートを定義するには、通常のクラス定義の先頭に template &lt;typename T&gt; のような記述を加えます。これは、関数テンプレートのときと同じなので、typename の代わりに classキーワードを使っても構いません。また、テンプレート仮引数の T が慣習的な名前であり、任意に付けて構わない点も同様です。

テンプレート仮引数として宣言された名前を、クラス定義内で型名の代わりに使用できます。もちろん、これらの名前は、テンプレート使用時に具体的な型名に置き換わります。

メンバ関数の定義を書く際にも、template &lt;typename T&gt; のような記述が必要です。コード例を見ると分かるように、非常に面倒臭いですが仕方がありません。もちろん、クラス定義内に直接、メンバ関数の定義を書くことはできますが、その場合、インライン展開する指示を与えていることになる点に注意してください(第12章参照)。

また、「Stack<T>::Pop()」のような感じで、クラステンプレート名の直後にもテンプレート仮引数の記述が必要なので、忘れないようにしてください。ここは少し表記法が違い、テンプレート仮引数の名前だけを書き並べます。

Stackクラステンプレートを使用する例は、次のようになります。

#include <iostream>
#include "Stack.h"

int main()
{
    Stack<int>         iStack(10);
    Stack<std::string> sStack(10);

    const std::size_t capacity = iStack.GetCapacity();
    for (std::size_t i = 0; i < capacity; ++i) {
        iStack.Push(static_cast<int>(i));
    }

    for (std::size_t i = 0; i < capacity; ++i) {
        std::cout << iStack.Top() << std::endl;
        iStack.Pop();
    }


    sStack.Push("aaa");
    sStack.Push("bbb");
    sStack.Push("ccc");

    const std::size_t size = sStack.GetSize();
    for (std::size_t i = 0; i < size; ++i) {
        std::cout << sStack.Top() << std::endl;
        sStack.Pop();
    }
}

実行結果:

9
8
7
6
5
4
3
2
1
0
ccc
bbb
aaa

「Stack<int>」のような表記で、クラステンプレートに具体的な型を当てはめられます。こうして、クラステンプレートを実体のあるものとして使用できます。

このサンプルプログラムの場合、変数iStack の型は Stack<int>型であり、変数sStack の型は Stack<std::string>型です。Stack型だとか、Stack<T>型だとは表現しません。こういった表現では、T の部分が不明確であり、まだ型として使える状態ではありません。

なお、関数テンプレートのときもそうでしたが(第9章)、クラステンプレートのメンバ関数の実体もヘッダファイル側に書く必要があります。ただし、第21章で説明するように、テンプレートの明示的なインスタンス化を利用することで、この制約を回避できます。

第9章のコラムでも書いたように、コンパイラによっては、このような制約がないこともあります。


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

クラステンプレートでは、テンプレート仮引数にデフォルトテンプレート実引数を指定できます

関数テンプレートの場合は、C++03 の時点ではデフォルトテンプレート実引数を与えることはできませんが、C++11 になって可能になりました(第9章)。

たとえば、Stackクラステンプレートが扱う型を、デフォルトで int型とするには、次のように書きます。

template <typename T = int>
class Stack {
    // 省略
};

通常の関数に与えるデフォルト実引数と同様に、デフォルトテンプレート実引数を与えられるのは、末尾側のテンプレート仮引数だけです第8章)。

この使い方は、Stack の要素が int型であることに必然性がないので、あまり良い使い方ではありませんが、一応、この例のように、テンプレート仮引数のところにデフォルトの型を指定すれば、デフォルト実引数を指定したことになります。

実際に使用する際には、次のように書きます。

Stack<> iStack(10);  // Stack<int>型

この例のように、テンプレート仮引数が1つだけしかなく、かつデフォルトテンプレート実引数を持っている場合でも、使用する際には「<>」の部分を省くことはできません

連続する山括弧の解釈

テンプレート実引数に、テンプレートを指定する場合などで、<> が入れ子になるような形になることがあります。次の例は、Stack<int> を要素とする Stack を作ろうとしています。

Stack<Stack<int>> isStack;  // エラー

このとき、>> の部分が、シフト演算子であると解釈されてしまうため、コンパイルエラーになります。そのため、正しく認識させるために>> の間にスペースを入れる必要があります。

Stack<Stack<int> > isStack;  // OK

C++11 (可変個テンプレート仮引数)

C++11

C++11 では、テンプレート仮引数の個数を可変にできるようになりました。 この機能については、第22章であらためて取り上げます。


typedefの活用

本章の最初に取り上げた Stackクラステンプレートを使った次のコードを見てください。

Stack<int> s(10);

// s へ要素をプッシュ

int value = s.Top();

プッシュしている部分は省略していますが、いくつかの要素を Pushメンバ関数で追加していると考えてください。

問題なのは最後の Topメンバ関数の部分です。戻り値を変数 value で受け取っていますが、この変数の型 int はどうやって決めたのでしょうか? もちろん、s を定義したとき、テンプレート実引数を int型にしているのですから、int型で受け取ることは正しいのですが、こういう書き方をしていると、後から型を変更しづらくなります。たとえば、次とおり。

Stack<long longg int> s(10);  // int から変更

// s へ要素をプッシュ

int value = s.Top();  // 切りつめられる。情報を失う危険がある

あらためて Topメンバ関数の宣言を確認してみると、次のように、戻り値の型は T になっています。

inline const T Top() const
{
    return mData[mSP - 1];
}

それならば、次のように書けば、テンプレート実引数の型の変更に自動的に対応できるでしょうか?

Stack<int> s(10);

// s へ要素をプッシュ

T value = s.Top();  // ?

これはコンパイルエラーになります。さすがに、このように唐突に T と書くのはダメだろうと思いますが、次のように修飾してみてもやはりダメです。

Stack<int> s(10);

// s へ要素をプッシュ

Stack<int>::T value = s.Top();  // ?

残念ながら、テンプレート仮引数の名前を、そのクラステンプレートの定義内以外から使うことはできません。そこで、クラステンプレート側で、後から使えるような名前を用意してやります。

template <typename T>
class Stack {
public:
    typedef T value_type;

    inline const value_type Top() const
    {
        return mData[mSP - 1];
    }

    // 今の話題に関係ないメンバは省略
};

追加された value_type という typedef名があれば、先ほどのプログラムコード片を次のように書き直せます。

Stack<int> s(10);

// s へ要素をプッシュ

Stack<int>::value_type value = s.Top();

これならばコンパイルが通ります。しかしこれだけだと、テンプレート実引数の型を変えたときに、2か所の書き換えが必要であることに変わりありません。「Stack<int>」が2回登場してしまっていることが問題です。そこで、今度は使用者側で次のように工夫を加えます。

typedef Stack<int> NumberStack;

NumberStack s(10);

// s へ要素をプッシュ

NumberStack::value_type value = s.Top();

ここでも typedef を使っています。これで、具体的な型名が typedef のところにしか登場しなくなりますから、int の部分を long long int とか、double とかに変えたとしても、既存のコードが影響を受けなくなります。このように typedef は、非常に重宝する存在です。特に、記述が長くなりがちな上、各所で繰り返し記述することが多いテンプレートの型は、基本的に typedef で別名を付けてから使うようにするのがです

typename指定子

前の項で、Stackクラステンプレートに value_type という型の別名を導入しました。基本的な考え方としてはこれで問題ないのですが、実際に使っていると、突然コンパイルエラーに見舞われることがあります。

無理やりな例ですが、次のプログラムを見てください。

class MyClass {
public:
    typedef int DataType;

    // 省略
};

template <typename T>
void func(T t)
{
    T::DataType* p;

    // 省略
}

int main()
{
    MyClass mc;
    func(mc);
}

このプログラムは問題ありません。しかし、「T::DataType* p;」の部分が、コンパイルエラーを起こす可能性があります。MyClassクラスの DataType の定義を次のように変更してみます。

    static const int DataType = 100;

DataType が typedef で定義される型名であれば、「T::DataType* p;」は T::DataType のポインタ型 p の変数を宣言していることになります。しかし、DataType が staticメンバ定数であると、「T::DataType* p;」は「100 * p;」という乗算をしているように見えてしまいます。そのため、p が未定義であるというコンパイルエラーになります。もし、p が別にあれば、コンパイルエラーにはならず、意図に反する結果を生んでしまいます。

int p = 3;  // これがあると…

template <typename T>
void func(T t)
{
    T::DataType* p;  // 100 * 3; は有効な文なのでエラーにならない

    // 省略
}

どこが問題のポイントなのかに注意してください。「T::DataType* p;」という文は、ポインタ変数 p を宣言しているのであって、それ以外の意図はないでしょうから、誤ったテンプレートの解釈は、コンパイルエラーになるようにしたいのです。

この問題を解決するには、「T::DataType」が型名であることを明示的に指示します。そのために typename指定子を使います。

template <typename T>
void func(T t)
{
    typename T::DataType* p;

    // 省略
}

typename指定子には、その直後に続く塊が、型名であることを明示する効果があります

ちなみに、テンプレート仮引数の記述では、typename を class で代用できますが、class には、後続を型名であると明示する効果はありません

同じ問題は、Topメンバ関数の定義を、クラステンプレートの定義の外で書こうとしたときにも起こります。

template <typename T>
const Stack<T>::value_type Stack<T>::Top() const
{
    return mData[mSP - 1];
}

戻り値が、クラス内に定義された型の場合には、クラス名による修飾が必要です。そのため、ここでは「Stack<T>::」による修飾が必要ですが、これもコンパイルエラーになってしまいます。そこでやはり、typename指定子を挟みます。

template <typename T>
const typename Stack<T>::value_type Stack<T>::Top() const
{
    return mData[mSP - 1];
}

std::string の正体

これまでそれほど気にせず使用してきた std::string (std::wstring も) ですが、この正体はクラステンプレートの typedef です。次のように定義されています。

namespace std {
    typedef basic_string<char> string;
    typedef basic_string<wchar_t> wstring;
}

std::string と std::wstring は、std::basic_stringクラステンプレートのテンプレート仮引数に、それぞれ char と wchar_t を当てはめて作られており、これに typedef を使って名前を付けたものです。

c_str関数や size関数、=演算子、[]演算子といったメンバ関数は、std::basic_stringクラステンプレートに定義されているので、std::string でも std::wstring でも共通で使用できます。


練習問題

問題① 標準ライブラリには、比較的単純なクラステンプレートとして std::pair があります。この章で取り上げた Stackクラステンプレートを使い、スタックの要素が std::pair であるようなプログラムを作成してください。
(std::pair については、【標準ライブラリ】第3章で解説しています)。

問題② 任意の型の3つの値を管理できるクラステンプレートを作ってみてください。値の管理に必要な最低限のメンバがあれば十分です。


解答ページはこちら

参考リンク


更新履歴

’2018/7/29 「C++11 (エイリアステンプレート)」の項の内容を削除。同じ内容を解説している Modern C++編のページへのリンクだけを残した。

’2018/7/21 typename を指定子と表記するように修正。

’2018/7/13 サイト全体で表記を統一(「静的メンバ」–>「staticメンバ」)

≪さらに古い更新履歴を展開する≫



前の章へ (第19章 演算子オーバーロード)

次の章へ (第21章 テンプレートのインスタンス化)

C++編のトップページへ

Programming Place Plus のトップページへ



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