この章の概要です。
クラステンプレートを使うと、メンバ変数や、メンバ関数^の引数・戻り値など、クラス定義の中で現れる型を、クラスの利用者が任意に決定できます。
クラステンプレートでなく、テンプレートクラスと呼ぶこともあります。大抵同じ意味で使われていますが、まれに、クラステンプレートに具体的な型を当てはめて作り出される具体的なクラスのことを指して、テンプレートクラスと呼んでいることがあります。今後は、クラステンプレートで統一します。
第6章で取り上げたように、クラスと構造体はほぼ同一であるといえるので、クラステンプレートの機能を、構造体に対して使うことも可能です。
例として、どんな型の数値でも3つ保存しておくことができて、合計や平均、最大値や最小値を返すメンバ関数を持ったクラステンプレートを定義してみます。
// Numbers.h
#ifndef NUMBERS_H_INCLUDED
#define NUMBERS_H_INCLUDED
template <typename T>
class Numbers {
public:
enum {
= 3 // 保存されている数値の個数
SIZE };
public:
(T n1, T n2, T n3);
Numbers
() const;
T Sum() const;
T Average() const;
T Max() const;
T Min
private:
[SIZE];
T mNumbers};
template <typename T>
<T>::Numbers(T n1, T n2, T n3)
Numbers{
[0] = n1;
mNumbers[1] = n2;
mNumbers[2] = n3;
mNumbers}
template <typename T>
<T>::Sum() const
T Numbers{
= 0;
T sum for (T n : mNumbers) {
+= n;
sum }
return sum;
}
template <typename T>
<T>::Average() const
T Numbers{
return Sum() / SIZE;
}
template <typename T>
<T>::Max() const
T Numbers{
= mNumbers[0];
T max for (int i = 1; i < SIZE; ++i) {
if (max < mNumbers[i]) {
= mNumbers[i];
max }
}
return max;
}
template <typename T>
<T>::Min() const
T Numbers{
= mNumbers[0];
T min for (int i = 1; i < SIZE; ++i) {
if (min > mNumbers[i]) {
= mNumbers[i];
min }
}
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()
{
<int> intNumbers(8, -4, 10);
Numbers<double> doubleNumbers(3.75, 12.5, -1.0);
Numbers
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
前述したとおり、クラステンプレート自体は型ではありません。テンプレート仮引数に具体的な型を当てはめることでクラスを作り出すことで、初めて型として機能します。ここで、クラステンプレートからクラスを作り出すことを指して、テンプレートのインスタンス化と呼びます。クラステンプレートをインスタンス化するには、次のように、テンプレート仮引数に対応する具体的な型を与えます。
<int> intNumbers(8, -4, 10);
Numbers<double> doubleNumbers(3.75, 12.5, -1.0); Numbers
1つ目の方は、テンプレート仮引数 T に int を、2つ目の方は double
を当てはめています。
ここで、テンプレート仮引数に対応するように与える情報を、テンプレート実引数と呼びます。
こうして、Numbers<int>型と
Numbers<double>型が作り出されました。
あとは普通のクラスと同じように使えば良いです。
このように、クラステンプレートを使うことで、クラス内で使ういくつかの型が、取り替え可能になります。一部の型だけが異なり、処理を実装するコード自体は同じになる場合に活用できます。
今度は、テンプレート仮引数が型でないパターンを取り上げます。 このようなテンプレート仮引数は、ノンタイプテンプレート仮引数(非型テンプレート仮引数)と呼ばれます。
テンプレート仮引数が型の場合には、テンプレート実引数は int や double のような型を指定しました。ノンタイプテンプレート仮引数の場合のテンプレート実引数は、100 や 1.25 のような定数を指定します。また、両方が混ざったクラステンプレートにもできます。
Numbersクラステンプレートを改造して、保存できる数値の個数を、テンプレート実引数で指定するようにしてみます。
// Numbers.h
#ifndef NUMBERS_H_INCLUDED
#define NUMBERS_H_INCLUDED
#include <cstddef>
template <typename T, std::size_t SIZE>
class Numbers {
public:
(const T* nums);
Numbers
() const;
T Sum() const;
T Average() const;
T Max() const;
T Min
private:
[SIZE];
T mNumbers};
template <typename T, std::size_t SIZE>
<T, SIZE>::Numbers(const T* nums)
Numbers{
for (std::size_t i = 0; i < SIZE; ++i) {
[i] = nums[i];
mNumbers}
}
template <typename T, std::size_t SIZE>
<T, SIZE>::Sum() const
T Numbers{
= 0;
T sum for (T n : mNumbers) {
+= n;
sum }
return sum;
}
template <typename T, std::size_t SIZE>
<T, SIZE>::Average() const
T Numbers{
return Sum() / SIZE;
}
template <typename T, std::size_t SIZE>
<T, SIZE>::Max() const
T Numbers{
= mNumbers[0];
T max for (int i = 1; i < SIZE; ++i) {
if (max < mNumbers[i]) {
= mNumbers[i];
max }
}
return max;
}
template <typename T, std::size_t SIZE>
<T, SIZE>::Min() const
T Numbers{
= mNumbers[0];
T min for (int i = 1; i < SIZE; ++i) {
if (min > mNumbers[i]) {
= mNumbers[i];
min }
}
return min;
}
#endif
// main.cpp
#include <iostream>
#include "Numbers.h"
int main()
{
const int nums[] = { 8, -4, 10, 6, -2 };
<int, 3> intNumbers(nums);
Numbers<int, 5> intNumbers2(nums);
Numbers
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つ目の制約のため、文字列リテラルも使えません。
型の別名を定義するために、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 };
<long, 5> numbers(nums);
Numbers
int sum = numbers.Sum(); // ?
この例の場合、Numbersクラステンプレートのテンプレート仮引数 T に当てはめられた実際の型は long型です。したがって、T型を返すように定義されている Sumメンバ関数の戻り値の型も long型になるのですが、間違って、int型で受け取ってしまっています。
普通、関数の戻り値を受け取る際には、関数の宣言を見て何型で返されるのか確認するでしょう。 今回のケースだと、そこには「T」と書かれている訳ですが、だからといって、次のようにはできません。
= numbers.Sum(); // コンパイルエラー。T が不明 T sum
クラステンプレートの外で「T」と書いても、何のことだか分かりませんから、コンパイルエラーになります。「T に当てはめた型」という型を表現する方法があれば、うまく書くことができるでしょうし、先ほどのように long型が適切なのに int と書いてしまうミスもなくせるはずです。その1つの方法が、クラステンプレート内で「公開」された型名を定義することです。
template <typename T, std::size_t SIZE>
class Numbers {
public:
using value_type = T;
public:
(const T* nums);
Numbers
() const;
T Sum
private:
[SIZE];
T mNumbers};
こうしておけば、クラステンプレートの利用者は、value_type という型名を、「T に当てはめた型」として使用できます。
const long nums[] = { 8, -4, 10, 6, -2 };
<long, 5> numbers(nums);
Numbers
<long, 5>::value_type sum = numbers.Sum(); Numbers
今度は Numbers<long, 5>
が繰り返し登場するようになってしまいました。 これも、using
で解決しましょう。
using LongNumbers = Numbers<long, 5>;
const LongNumbers::value_type nums[] = { 8, -4, 10, 6, -2 };
(nums);
LongNumbers numbers
::value_type sum = numbers.Sum(); LongNumbers
これで、型を間違える危険が小さくなりました。また、後から型を変えたとしても、Sumメンバ関数の戻り値を受け取る変数の型の方も自動的に変わります。
今回のケースでは、Sumメンバ関数の戻り値を受け取る変数の型を、auto(第18章)にしたり、decltype(第18章)を使用したりすることでも対応できます。
前の項で、value_type型を導入したとき、Sumメンバ関数の宣言も変更して、value_type型を返すようにしてはどうでしょう? つまり、次のようにします。
template <typename T, std::size_t SIZE>
class Numbers {
public:
using value_type = T;
public:
(const T* nums);
Numbers
value_type Sum() const;
private:
[SIZE];
T mNumbers};
template <typename T, std::size_t SIZE>
<T, SIZE>::value_type Numbers<T, SIZE>::Sum() const
Numbers{
value_type sum = 0;
for (value_type n : mNumbers) {
+= n;
sum }
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) {
+= n;
sum }
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 };
<int> iNumbers(integers);
FiveNumbers<float> fNumbers(floats);
FiveNumbers
std::cout << iNumbers.Sum() << std::endl;
std::cout << fNumbers.Sum() << std::endl;
}
実行結果:
18
-1.5
なお、エイリアステンプレートを main関数の内側のような関数内には記述できないことに注意してください。 エイリアステンプレートは、新たなクラステンプレートを定義しているのと同じことなので、 クラステンプレートの定義が書ける場所にしか書けません。
問題① 生徒を表す Studentクラスで、国語、英語、数学の得点を管理したいとします。 テンプレート仮引数 T と SIZE を持つ Numbersクラステンプレートを用いて、実現してください。 (問題に関係がない部分は、任意で構いません)。
問題② 任意の型の3つの値を管理できるクラステンプレートを作ってみてください。
typename を指定子と表記するように修正。
「サイズ」という表記について表現を統一。 型のサイズ(バイト数)を表しているところは「大きさ」、要素数を表しているところは「要素数」。
新規作成。
Programming Place Plus のトップページへ
はてなブックマーク に保存 | Pocket に保存 | Facebook でシェア |
X で ポスト/フォロー | LINE で送る | noteで書く |
RSS | 管理者情報 | プライバシーポリシー |