Modern C++編【言語解説】 第8章 クラステンプレート

先頭へ戻る

このエントリーをはてなブックマークに追加

この章の概要

この章の概要です。

クラステンプレート

クラステンプレートを使うと、メンバ変数や、メンバ関数の引数・戻り値など、 クラス定義の中で現れる型を、クラスの利用者が任意に決定できるようになります。

クラステンプレートでなく、テンプレートクラスと呼ぶこともあります。 大抵同じ意味で使われていますが、 稀に、クラステンプレートに具体的な型を当てはめて作り出される具体的なクラスのことを指して、 テンプレートクラスと呼んでいることがあります。
当サイトでは、クラステンプレートで統一します。

第6章で取り上げたように、 クラスと構造体はほぼ同一のものと言えるので、クラステンプレートの機能を、構造体に対して使うことも可能です。

例として、どんな型の数値でも3つ保存しておくことができて、 合計や平均、最大値や最小値を返すメンバ関数を持ったクラステンプレートを定義してみます。

// Numbers.h

#ifndef NUMBERS_H
#define NUMBERS_H

template <typename T>
class Numbers {
public:
    enum {
        SIZE = 3  // 保存されている数値の個数
    };

public:
    Numbers(T n1, T n2, T n3);

    T Sum() const;
    T Average() const;
    T Max() const;
    T Min() const;

private:
    T  mNumbers[SIZE];
};


template <typename T>
Numbers<T>::Numbers(T n1, T n2, T n3)
{
    mNumbers[0] = n1;
    mNumbers[1] = n2;
    mNumbers[2] = n3;
}

template <typename T>
T Numbers<T>::Sum() const
{
    T sum = 0;
    for (T n : mNumbers) {
        sum += n;
    }
    return sum;
}

template <typename T>
T Numbers<T>::Average() const
{
    return Sum() / SIZE;
}

template <typename T>
T Numbers<T>::Max() const
{
    T max = mNumbers[0];
    for (int i = 1; i < SIZE; ++i) {
        if (max < mNumbers[i]) {
            max = mNumbers[i];
        }
    }
    return max;
}

template <typename T>
T Numbers<T>::Min() const
{
    T min = mNumbers[0];
    for (int i = 1; i < SIZE; ++i) {
        if (min > mNumbers[i]) {
            min = mNumbers[i];
        }
    }
    return min;
}

#endif

クラステンプレートを定義するには、通常のクラス定義の構文の先頭部分に「template <typename T>」のような表記を付けますtypename の部分は、代わりに class を使っても構いません。

typename(または class)に続く名前「T」は、テンプレートパラメータ(テンプレート仮引数)と呼ばれます。 T という名前は慣習的なもので、任意に別の名前を付けて構いません。 勿論、テンプレートパラメータは複数個になっても良いです。

template <typename T1, typename T2, typename T3>
class Numbers {
}

後で取り上げますが、クラステンプレートを使用するときに、テンプレートパラメータの部分に当てはめる具体的な型を指定します。 仮に「int」を当てはめるとすれば、クラステンプレート内にあるすべての「T」が「int」に置き換わった状態になります。
「T」という曖昧な型が無くなり、すべて「int」のような具体的な型に置き換わってしまえば、 最早、普通のクラスと同じになります。 つまり、クラステンプレートは、クラスを作り出す雛形(テンプレート)である訳です。 クラステンプレート自体は型ではなく、そこから作り出されたクラスは型です。

メンバ関数の定義を書く際にも、「template <typename T>」のような表記が必要です。 また、「Numbers<T>::Sum()」のように、クラステンプレート名の直後にも、テンプレートパラメータを記述しなければなりません。 ただしここには、typename や classキーワードは書かず、テンプレートパラメータの名前だけを並べます。

なお、クラステンプレートのメンバ関数は、通常、ヘッダファイルに記述します

コンパイラによっては、ソースファイル側に記述できるものもあるかも知れません。

では、Numbersクラステンプレートを使う側を見ていきましょう。 例えば、次のようになります。

// main.cpp

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

int main()
{
    Numbers<int> intNumbers(8, -4, 10);
    Numbers<double> doubleNumbers(3.75, 12.5, -1.0);

    std::cout << "sum: " << intNumbers.Sum() << "\n"
              << "avg: " << intNumbers.Average() << "\n"
              << "max: " << intNumbers.Max() << "\n"
              << "min: " << intNumbers.Min() << std::endl;
    std::cout << "sum: " << doubleNumbers.Sum() << "\n"
              << "avg: " << doubleNumbers.Average() << "\n"
              << "max: " << doubleNumbers.Max() << "\n"
              << "min: " << doubleNumbers.Min() << std::endl;
}

実行結果:

sum: 14
avg: 4
max: 10
min: -4
sum: 15.25
avg: 5.08333
max: 12.5
min: -1

前述した通り、クラステンプレート自体は型ではありません。 テンプレートパラメータに具体的な型を当てはめることでクラスを作り出すことで、初めて型として機能します。 ここで、クラステンプレートからクラスを作り出すことを指して、テンプレートのインスタンス化と呼びます。 クラステンプレートをインスタンス化するには、次のように、テンプレートパラメータに対応する具体的な型を与えます。

Numbers<int> intNumbers(8, -4, 10);
Numbers<double> doubleNumbers(3.75, 12.5, -1.0);

1つ目の方は、テンプレートパラメータ T に int を、2つ目の方は double を当てはめています。 ここで、テンプレートパラメータに対応するように与える情報を、テンプレート実引数と呼びます。
こうして、Numbers<int>型と Numbers<double>型が作り出されました。 あとは普通のクラスと同じように使えば良いです。

このように、クラステンプレートを使うことで、クラス内で使う幾つかの型について、取り替え可能になります。 一部の型だけが異なり、処理を実装するコード自体は同じになる場合に活用できます。

ノンタイプテンプレートパラメータ

今度は、テンプレートパラメータが型でないパターンを取り上げます。 このようなテンプレートパラメータは、ノンタイプテンプレートパラメータ(非型テンプレートパラメータ)と呼ばれます。

テンプレートパラメータが型の場合には、テンプレート実引数は int や double のような型を指定しました。 ノンタイプテンプレートパラメータの場合のテンプレート実引数は、100 や 1.25 のような定数を指定します。 また、両方が混ざったクラステンプレートにすることもできます。

Numbersクラステンプレートを改造して、保存できる数値の個数を、テンプレート実引数で指定するようにしてみます。

// Numbers.h

#ifndef NUMBERS_H
#define NUMBERS_H

#include <cstddef>

template <typename T, std::size_t SIZE>
class Numbers {
public:
    Numbers(const T* nums);

    T Sum() const;
    T Average() const;
    T Max() const;
    T Min() const;

private:
    T  mNumbers[SIZE];
};


template <typename T, std::size_t SIZE>
Numbers<T, SIZE>::Numbers(const T* nums)
{
    for (std::size_t i = 0; i < SIZE; ++i) {
        mNumbers[i] = nums[i];
    }
}

template <typename T, std::size_t SIZE>
T Numbers<T, SIZE>::Sum() const
{
    T sum = 0;
    for (T n : mNumbers) {
        sum += n;
    }
    return sum;
}

template <typename T, std::size_t SIZE>
T Numbers<T, SIZE>::Average() const
{
    return Sum() / SIZE;
}

template <typename T, std::size_t SIZE>
T Numbers<T, SIZE>::Max() const
{
    T max = mNumbers[0];
    for (int i = 1; i < SIZE; ++i) {
        if (max < mNumbers[i]) {
            max = mNumbers[i];
        }
    }
    return max;
}

template <typename T, std::size_t SIZE>
T Numbers<T, SIZE>::Min() const
{
    T min = mNumbers[0];
    for (int i = 1; i < SIZE; ++i) {
        if (min > mNumbers[i]) {
            min = mNumbers[i];
        }
    }
    return min;
}

#endif
// main.cpp

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

int main()
{
    const int nums[] = { 8, -4, 10, 6, -2 };
    Numbers<int, 3> intNumbers(nums);
    Numbers<int, 5> intNumbers2(nums);

    std::cout << "sum: " << intNumbers.Sum() << "\n"
              << "avg: " << intNumbers.Average() << "\n"
              << "max: " << intNumbers.Max() << "\n"
              << "min: " << intNumbers.Min() << std::endl;
    std::cout << "sum: " << intNumbers2.Sum() << "\n"
              << "avg: " << intNumbers2.Average() << "\n"
              << "max: " << intNumbers2.Max() << "\n"
              << "min: " << intNumbers2.Min() << std::endl;
}

実行結果:

sum: 14
avg: 4
max: 10
min: -4
sum: 18
avg: 3
max: 10
min: -4

ノンタイプテンプレートパラメータの場合は、型は最初から決まっているので、 typename(または class)ではなく、具体的な型名を記述します。 ここでは、C/C++ においてサイズを表現するときに使われる size_t型(std::size_t) を指定しました。

ノンタイプテンプレートに対応するテンプレート実引数は、型に合った定数値を指定します。 定数であれば良いので「5 + 10」のような指定でも構いません。

第24章で解説する constexpr を使えば、関数が返す値を定数として扱うことができます。

「Numbers<int, 3>」であれば、クラステンプレート内の「T」に「int」を、 「SIZE」に「3」を当てはめたクラスがインスタンス化されます。

なお、ノンタイプテンプレートパラメータには制約があり、 浮動小数点型、void型、クラス型、内部結合されるオブジェクトを指しているポインタは使えません。 4つ目の制約のため、文字列リテラルも使えません

using による型の別名の定義

型の別名を定義するために、C言語では typedef を用いましたが、 C++ には using を用いる方法もあります。 例えば、以下の2つは同じ意味になります。

using 新しい型名 = 既存の型名;
typedef 既存の型名 新しい型名;

typedef は、型の名前が2つ並べられているだけなので、一見してどちらが新しく定義された名前なのか分かりづらいですが、 using は「=」が入ることで、初期化や代入の構文に見えるため、左側に来る方が新しい名前だと分かりやすいでしょう。 これは特に、関数ポインタ型を定義する際に顕著です。 以下の2つは同じ意味ですが、typedef の方は、慣れていないと非常に理解しづらいと思います。

using getter = const char*(*)(int);
typedef const char* (*getter)(int);

また、using を使った方法は、後で紹介するエイリアステンプレートの実現にも使えます。 これに関しては typedef では出来ません。

using や typedef を使って、クラステンプレート内に「公開」された型名を用意すると、 利用者にとって便利になることがあります。 Numbersクラステンプレートを使った次の例で考えてみましょう。

const long nums[] = { 8, -4, 10, 6, -2 };
Numbers<long, 5> numbers(nums);

int sum = numbers.Sum();  // ?

この例の場合、Numbersクラステンプレートのテンプレートパラメータ T に当てはめられた実際の型は long型です。 従って、T型を返すように定義されている Sum() の戻り値の型も long型になるのですが、 間違って、int型で受け取ってしまっています。

普通、関数の戻り値を受け取る際には、関数の宣言を見て何型で返されるのか確認するでしょう。 今回のケースだと、そこには「T」と書かれている訳ですが、だからといって、次のようにはできません。

T sum = numbers.Sum();  // コンパイルエラー。T が不明

クラステンプレートの外で「T」と書いても、何のことだか分かりませんから、コンパイルエラーになります。 「T に当てはめた型」という型を表現する方法があれば、うまく書くことができるでしょうし、 先ほどのように long型が適切なのに int と書いてしまうミスも無くせるはずです。 その1つの方法が、クラステンプレート内で「公開」された型名を定義することです。

template <typename T, std::size_t SIZE>
class Numbers {
public:
    using value_type = T;

public:
    Numbers(const T* nums);

    T Sum() const;

private:
    T  mNumbers[SIZE];
};

こうしておけば、クラステンプレートの利用者は、value_type という型名を、「T に当てはめた型」として使用できます

const long nums[] = { 8, -4, 10, 6, -2 };
Numbers<long, 5> numbers(nums);

Numbers<long, 5>::value_type sum = numbers.Sum();

今度は「Numbers<long, 5>」が繰り返し登場するようになってしまいました。 これも、using で解決しましょう。

using LongNumbers = Numbers<long, 5>;

const LongNumbers::value_type nums[] = { 8, -4, 10, 6, -2 };
LongNumbers numbers(nums);

LongNumbers::value_type sum = numbers.Sum();

これで、型を間違える危険が小さくなりました。 また、後から型を変えたとしても、Sum() の戻り値を受け取る変数の型の方も自動的に変わります。

今回のケースでは、Sum() の戻り値を受け取る変数の型を、auto(第18章)にしたり、 decltype(第18章)を使用したりすることでも対応できます。

typenameキーワード

前の項で、value_type型を導入したとき、Sum() の宣言も変更して、 value_type型を返すようにしてはどうでしょう? つまり、次のようにします。

template <typename T, std::size_t SIZE>
class Numbers {
public:
    using value_type = T;

public:
    Numbers(const T* nums);

    value_type Sum() const;

private:
    T  mNumbers[SIZE];
};

template <typename T, std::size_t SIZE>
Numbers<T, SIZE>::value_type Numbers<T, SIZE>::Sum() const
{
    value_type sum = 0;
    for (value_type n : mNumbers) {
        sum += n;
    }
    return sum;
}

ところがこれは、Sum() の定義の方でコンパイルエラーになってしまいます。 テンプレートパラメータが絡む名前で修飾される場合(Numbers<T, SIZE>:: の部分)、 それが型であることを、typenameキーワードを使って明確にしなければならないというルールがあるためです。 そのため、以下のように typenameキーワードを補う必要があります。

template <typename T, std::size_t SIZE>
typename Numbers<T, SIZE>::value_type Numbers<T, SIZE>::Sum() const
{
    value_type sum = 0;
    for (value_type n : mNumbers) {
        sum += n;
    }
    return sum;
}

このように typenameキーワードを補わなければならない場面が幾つかありますが、 コンパイラが、分かりやすいエラーメッセージで教えてくれないこともあるので、よく覚えておいて下さい。

エイリアステンプレート

クラステンプレートと using を使って、型の別名を定義することができます。 この機能は、エイリアステンプレートと呼ばれます。 以下は、使用例です。

// 名前を変えるだけが目的
template <typename T, std::size_t SIZE>
using Nums = Numbers<T, SIZE>;

// 一部のテンプレート実引数を固定する
template <typename T>
using FiveNumbers = Numbers<T, 5>;

Nums の方は、Numbers よりも短い別名を作る目的で定義しています。 FiveNumbers の方はテンプレート実引数の一部を固定化します。 後者の具体的なサンプルプログラムを挙げます。

// main.cpp

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

template <typename T>
using FiveNumbers = Numbers<T, 5>;

int main()
{
    const int integers[] = { 8, -4, 10, 6, -2 };
    const float floats[] = { 3.5f, 1.5f, -2.0f, -7.5f, 3.0f };

    FiveNumbers<int> iNumbers(integers);
    FiveNumbers<float> fNumbers(floats);

    std::cout << iNumbers.Sum() << std::endl;
    std::cout << fNumbers.Sum() << std::endl;
}

実行結果:

18
-1.5

なお、エイリアステンプレートを main関数の内側のような関数内には記述できないことに注意して下さい。 エイリアステンプレートは、新たなクラステンプレートを定義しているのと同じことなので、 クラステンプレートの定義が書ける場所にしか書けません。


練習問題

問題① 生徒を表す Studentクラスで、国語、英語、数学の得点を管理したいとします。 テンプレートパラメータ T と SIZE を持つ Numbersクラステンプレートを用いて、実現して下さい。 (問題に関係の無い部分は、任意で構いません)。

問題② 任意の型の3つの値を管理できるクラステンプレートを作ってみて下さい。


解答ページはこちら

参考リンク

更新履歴

'2017/7/23 新規作成。



前の章へ

次の章へ

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

Programming Place Plus のトップページへ