テンプレートの機能
C++の関数やクラスにはテンプレートという、その処理内で必要となるデータ型を呼び出し側から指定できる機能があります。
ここではテンプレートの機能をいくつか説明します。
テンプレートの基本的な使い方
まずはテンプレートの基本的な使い方を簡単におさらいしておきます。
関数テンプレートの項も参照してください。
//関数テンプレート
//型テンプレート引数「T」が具体的なデータ型に置き換わる
template<typename T>
T func(T a, T b)
{
return a + b;
}
int main()
{
//引数からの型の推定
int a = func(1, 2);
double b = func(3.0, 4.0);
//型推論があいまいなのでエラー
//func(5, 6.0);
//型テンプレート引数の明示的な指定
int c = func<int>(5, 6.0);
double d = func<double>(7, 8.0);
}
デフォルト引数
関数の引数と同様に、テンプレート仮引数にはデフォルト値(初期値)を与えることができます。
template<typename T = int>
T func(T a = T()) { return a; }
int main()
{
func(); //int型
func<>(); //int型
func<double>(); //double型(型テンプレート引数の明示的指定)
func(1.0); //double型(関数の実引数からの型推論)
}
型の明示的な指定、および関数の実引数から型推論が可能な場合はそちらが優先されます。
上記コードを少し捕捉すると、仮引数のT a = T()
はT
型のデフォルトコンストラクタの呼び出しです。
仮引数なので、それを引数のデフォルト値に設定しています。
template<typename T>
T func(T a = T()) { return a; }
T
型が基本型の場合、算術型(数値型)では0
、bool型ではfalse
になります。
デフォルトコンストラクタを持たないクラスをT
型に指定するとコンパイルエラーになります。
関数のデフォルト引数と同じく、デフォルト引数はすべての非デフォルト引数よりも後方に指定する必要があります。
//NG
template<typename T = int, typename U>
void func(T a, U b) { /*省略*/ }
//OK
template<typename T, typename U = int>
void func(T a, U b) { /*省略*/ }
なお、デフォルト引数はクラステンプレートではC++03から使用可能ですが、関数テンプレートの場合はC++11から可能です。
クラステンプレート
テンプレートは関数だけでなくクラスにも使用することができます。
#include <iostream>
template<typename T>
class TestClass
{
public:
T x;
TestClass(T x = T())
: x(x) {}
};
int main()
{
TestClass<int> tcInt; //明示的なデータ型の指定
TestClass tcDouble(0.123); //型推論(C++17)
//コンパイルエラー
//コンストラクタ引数がない場合は型推論できない
//TestClass tc;
std::cout << tcInt.x << std::endl;
std::cout << tcDouble.x << std::endl;
std::cin.get();
}
記述方法は関数テンプレートとほぼ同じです。
上記の例ではテンプレート仮引数のT
がint型やdouble型などの具体的なデータ型に置き換わります。
C++17以降では、データ型はコンストラクタの引数から型推論することもできます。
メンバ関数をクラス外部に定義する場合
テンプレート仮引数が必要なメンバ関数をクラスの外で定義する場合、その関数定義にもtemplate
キーワードが必要になります。
template<typename T>
class TestClass
{
public:
T x;
TestClass(T x = T())
: x(x) {}
void add(T); //メンバ関数の宣言
};
//メンバ関数の定義
template<typename T>
void TestClass<T>::add(T a)
{
x += a;
}
非型テンプレート仮引数
テンプレート仮引数にはデータ型以外にも値を指定することができます。
これを非型テンプレート仮引数(ノンタイプテンプレート仮引数)といいます。
データ型を指定するための仮引数は型テンプレート仮引数と言います。
#include <iostream>
//Tは型テンプレート仮引数
//SIZEは非型テンプレート仮引数
template<typename T, size_t SIZE>
class TestClass
{
T arr[SIZE];
public:
T& operator[](size_t index) const
{
return arr[index];
}
T& operator[](size_t index)
{
return arr[index];
}
void print() const
{
for (size_t n = 0; n < SIZE; n++)
{
std::cout << arr[n] << ", ";
}
std::cout << std::endl;
}
};
int main()
{
const size_t SIZE = 5;
TestClass<int, SIZE> tc;
for (size_t n = 0; n < SIZE; n++) {
tc[n] = n;
}
tc.print();
//0, 1, 2, 3, 4,
}
このTestClass
クラステンプレートで使用しているテンプレート仮引数のT
は通常のデータ型の指定です。
その次のSIZE
が非型テンプレート仮引数で、typename
(またはclass
)キーワードではなく使用したいデータ型名を仮引数の手前に記述します。
非型テンプレート仮引数に指定した値は、そのテンプレート内では定数のように扱うことができます。
呼び出し元(テンプレート実引数)には定数式(コンパイル時定数)のみを指定できます。
//NUMBERは非型テンプレート仮引数
template<int NUMBER>
class MyClass
{
public:
void Print()
{
//書き換えはできない
//NUMBER = 3;
std::cout << NUMBER << std::endl;
}
};
int func() { return 3; }
int main()
{
int n = 1;
const int cn1 = 2; //数値リテラルのconst定数はコンパイル時定数
const int cn2 = func(); //それ以外のconst定数は実行時定数
//全てコンパイル時定数なのでOK
MyClass<10> mc1;
MyClass<1 + 2> mc2;
MyClass<cn1> mc3;
//MyClass<cn2> mc4; //実行時時定数はNG
//MyClass<n> mc5; //変数はNG
}
テンプレート実引数が異なるクラス同士は別のクラスとなり、そのインスタンス同士は互換性がありません。
template<int NUMBER>
class MyClass{ /*省略*/ };
int main()
{
MyClass<10> mc1;
MyClass<10> mc2;
MyClass<20> mc3;
mc2 = mc1; //OK
//mc3 = mc1; //NG
}
非型テンプレート仮引数に指定できるデータ型は「整数型」「列挙型」「ポインタ型」「参照型」です。
C++11以降は「std::nullptr_t
」、C++17以降は「プレースホルダ型(autoのこと)」も指定可能です。
C++20以降は浮動小数点数型(要するに小数型)と、以下の条件を満たすリテラルクラス型も使用可能です。
- 全ての基底クラスおよび非静的メンバ変数がpublic、かつ書き換え不可であること
- 全ての基底クラスおよび非静的メンバ変数が構造的型、あるいはその配列であること
(構造的型=算術型、列挙型、ポインタ型、std::nullptr_t
、および左辺値参照型であること)
非型テンプレート実引数にポインタ型と参照型を指定する場合、参照する先は静的領域(グローバル、static)かNULLポインタ(nullptr)である必要があります。
文字列リテラル、一時オブジェクト、配列の要素などは指定できません。
テンプレートテンプレート仮引数
型テンプレート仮引数には「データ型」を指定できます。
ここには自作のクラス型を指定することもできますが、クラステンプレートは指定できません。
クラステンプレートは「テンプレート(ひな形)」であって「クラス」ではないためです。
テンプレートに別のテンプレートを渡すにはテンプレートテンプレート仮引数という機能を使用します。
#include <iostream>
//クラステンプレート
template<typename T>
class TClass
{
T a;
public:
TClass(int n = 0) : a(n) {}
T get()
{
return a;
}
};
//テンプレートテンプレート仮引数を利用した
//クラステンプレート
template<template<typename U> class T>
class TTClass
{
T<int> a;
public:
TTClass(int n = 0) : a(n) {}
void print()
{
std::cout << a.get() << std::endl;
}
};
int main()
{
TTClass<TClass> ttc(10);
ttc.print();
//10
}
上記コードのTTClass
クラステンプレートでテンプレートテンプレート仮引数を使用しています。
template<template<typename U> class T>
class TTClass{/**省略**/}
一見するとテンプレート仮引数がふたつあるよう見えますが、template<typename U> class T
でひとまとまりの宣言で、ひとつのクラステンプレートT
を受け取ることを示します。
テンプレート内で使用するのは型引数T
のみで、U
は型引数ではなく書式の一部です。
このU
は使用しないので省略することができます。
//内側のテンプレート仮引数は無くても良い
template<template<typename> class T>
class TTClass{/**省略**/}
通常のテンプレート仮引数の宣言ではtypename
とclass
のどちらも使用することができますが、テンプレートテンプレート仮引数の場合はclass
のみを使用できます。
C++17以降はtypename
も使用可能です。
上記サンプルコードではTTClass
内でテンプレート実引数の型にint型を指定していますが、呼び出し元からデータ型を指定する場合はテンプレート仮引数をひとつ増やします。
template<typename T>
class TClass{ /*省略*/ };
template<template<typename> class T, typename U>
class TTClass
{
T<U> a;
public:
//省略
};
int main()
{
TTClass<TClass, char> ttc;
}
宣言内の仮引数の数について
C++17よりも以前のバージョンでは、テンプレートテンプレート仮引数の(ひとつの宣言内における)仮引数の数は、使用するクラステンプレートのテンプレート仮引数の数と正確に一致する必要があります。
//C++14まで
template<typename T>
class T1 { /*省略*/ };
template<typename T, typename U>
class T2 { /*省略*/ };
template<typename T, typename U = int>
class T3 { /*省略*/ };
//テンプレート仮引数がひとつのクラステンプレートを渡せる
template<template<typename> class T>
class TT1 { /*省略*/ };
//テンプレート仮引数がふたつのクラステンプレートを渡せる
template<template<typename, typename> class T>
class TT2 { /*省略*/ };
int main()
{
TT1<T1> a;
//TT1<T2> b; //エラー
//TT1<T3> c; //エラー
//TT2<T1> d; //エラー
TT2<T2> e;
TT2<T3> f;
}
上記のクラステンプレートTT1
のテンプレートテンプレート仮引数が受け取れるのは「仮引数ひとつ」のテンプレートのみです。
クラステンプレートT3
はデフォルト引数によってテンプレート実引数ひとつで使用することができますが、宣言の仮引数はふたつあるので、これをTT1
のテンプレートテンプレート仮引数に渡すことはできません。
C++17以降はこれが緩和され、呼び出し可能な実引数の数と一致する場合はそのテンプレートを渡すことができます。
//C++17以降
//クラステンプレートの定義は上のサンプルコートと同じなので省略
int main()
{
TT1<T1> a;
//C1<T2> b; //エラー
TT1<T3> c; //C++17以降はOK
//C2<T1> d; //エラー
TT2<T2> e;
TT2<T3> f;
}
なお、vectorクラスなどのSTLコンテナもクラステンプレートであり、これを自作のテンプレートに渡すにはテンプレートテンプレート仮引数を使用します。
その場合はテンプレート仮引数の数に注意が必要です。
例えばvectorクラスのテンプレート仮引数は実は第二引数にデフォルト値が設定されていて、ほとんどの場合で省略した形で使用されます。
そのため、C++14以前では自作テンプレート側の仮引数の数を合わせる必要があります。
//vectorクラスの定義の抜粋
//Allocatorはメモリを確保処理を担うクラス型
//普段は省略されることが多いが独自クラスを指定して管理することもできる
template <class T, class Allocator = allocator<T>>
class vector;
//--
//vectorを受け取る場合
//C++14以前の形式
template<template<typename, typename> class T>
class C1 { /*省略*/ };
//C++17以降はこれでOK
template<template<typename> class T>
class C2 { /*省略*/ };
可変引数テンプレート
関数の仮引数やテンプレート仮引数は、あらかじめ定義した通りの数の引数を受け取ります。
C++11からは可変引数テンプレートという機能により、呼び出し元で任意の数の引数を指定することができます。
#include <iostream>
void funcX(int a, char b, const char* c)
{
std::cout << a << ", ";
std::cout << b << ", ";
std::cout << c << std::endl;
}
//通常の関数テンプレート
template<typename T>
void func1(T t)
{
std::cout << t << std::endl;
}
//可変引数テンプレート
template<typename... Ts>
void func2(Ts... ts)
{
funcX(ts...);
}
int main()
{
func1(123);
func2(123, 'a', "abc");
std::cin.get();
}
123 123, a, abc
上記コードのfunc1
は通常の関数テンプレート、func2
は可変引数の関数テンプレートです。
可変引数テンプレートは、異なるデータ型の引数をいくつでも指定することができます。
なお、受け取れる引数の数は0個以上なので何も指定しない場合でも呼び出すことができます。
書式
可変引数テンプレートには...
という、ピリオドを3個並べた省略記号(エリプシス(ellipsis))というものを使用します。
省略記号は記述可能な場所が決まっていて、記述場所により機能が異なります。
template<typename... Args> //1
void f1(Args... args) //2
{
//3
f2(args...);
Args arr[] = { args... };
[args...] {}; //ラムダ式
//4
std::tuple<Args...> tuple;
//5、エラー
//args...;
}
- テンプレート仮引数の
typename
またはclass
の直後
(非型テンプレート引数の場合はデータ型名の直後) - 関数の仮引数宣言で1のテンプレート仮引数を指定した直後
- 関数内で1または2の名前を使用した直後
上記のArgs
(1)やargs
(2)はパラメーターパックといい、呼び出し元から渡された複数の引数がひとつまとめられた状態になっています。
Args
はデータ型のパラメーターパックで、args
はそのデータ型の値のパラメーターパックです。
3はパラメーターパックを展開する処理で、関数やクラスコンストラクタの実引数、波括弧初期化子、ラムダ式のキャプチャなどに指定することができます。
展開後は呼び出し元で指定した引数の順番通りのデータになります。
4は呼び出し元で指定されたテンプレート実引数の順番通りにデータ型が展開されます。
5のように、展開可能な位置以外でパラメータパックを展開することはできません。
上記のほか、クラスの継承時の基底クラスの指定と、クラスのメンバイニシャライザでもパラメーターパックの展開が可能です。
template<class... Bases>
class C : public Bases...
{
public:
C(const Bases&... bases) : Bases(bases)... { }
};
テンプレート宣言内のパラメーターパックの位置
テンプレート仮引数が複数ある場合、パラメーターパックはひとつだけ指定可能で、全てのテンプレート仮引数の最後に指定する必要があります。
//OK
template<typename T, typename... U>
void f1() {};
//NG
template<typename... T, typename U>
void f2() {};
sizeof...演算子
パラメーターパックに含まれている要素の数はsizeof...
演算子で調べることができます。
#include <iostream>
template<typename... Ts>
void func(Ts... ts)
{
std::cout << sizeof...(Ts) << ", " << sizeof...(ts) << std::endl;
}
int main()
{
func(123);
func(123, 'a');
func(123, 'a', "abc");
std::cin.get();
}
1, 1 2, 2 3, 3
再帰を利用した展開データの取り出し
最初のサンプルコードでは、関数呼び出し元の実引数を受け取れる別の関数(funcX
)にそのまま渡しているだけで実用的なコードではありません。
パラメーターパックから元の実引数をひとつずつ取り出して処理を行うには再帰を利用します。
#include <iostream>
//何もしない同名関数をオーバーロード
//再帰呼び出しの最後にこの関数が呼ばれる
void func() {}
//パラメーターパックの先頭要素はHead、それ以降はTailに格納される
template<typename Head, typename... Tail>
void func(Head head, Tail... tail)
{
std::cout << head << std::endl;
func(tail...); //tailを展開して再帰呼び出し
}
int main()
{
func(123, 'a', "abc");
}
123 a abc
最初のテンプレート仮引数は通常の形式で宣言し、二番目はパラメーターパック形式で宣言します。
こうすることで、受け取った実引数の第一引数が先頭のテンプレート仮引数に、以降の引数は全てパラメーターパックとして受け取ることができます。
関数内ではパラメーターパックを展開したものを引数にして再帰呼び出しします。
再帰呼び出しのたびにパラメーターパックからひとつずつ引数が取り出され、最終的に空(0個の要素)のパラメーターパックを受け取ることになります。
そのままでは呼び出せる関数がないので、引数を受け取らず何も処理をしない関数オーバーロードを定義しておきます。
sizeof...
演算子とif文で終了を検知することもできますが、効率の面からあまり好まれないようです。
パラメーターパックの拡張
パラメーターパックの展開のための省略記号は、パック名に直接付けてデータを展開するほかに、パック名が含まれる式にも付けることができます。
こうすると展開後のそれぞれの値に共通の処理を適用することができます。
これをパラメーターパックの拡張と言います。
以下のコードのコメント内では、展開されるデータをE
とし、データ名を順にE1
、E2
、E3
…と記述します。
template<typename T>
void g(T a, T b, T c)
{
std::cout
<< a << ", "
<< b << ", "
<< c << std::endl;
}
template<typename T>
T pow(T a) {
return a * a;
}
template<typename ...Args>
void f(Args... args)
{
g(args...); //g(E1, E2, E3);
g(++args...); //g(++E1, ++E2, ++E3);
g(pow(args)...); //g(pow(E1), pow(E2), pow(E3));
//リスト初期化を記述するためのダミー配列
//std::cout << E1 << " ", std::cout << E2 << " ", std::cout << E3 << " ";
int dummy[] = { (std::cout << args << " ", 0)...};
}
int main()
{
f(1, 2, 3);
}
1, 2, 3 2, 3, 4 4, 9, 16 2 3 4
ひとつの式に複数のパラメーターパックが含まれている場合は同時に展開されます。
このとき、パラメーターパックに含まれる要素数は同じである必要があります。
template <typename... Args1>
struct S
{
template <typename... Args2>
struct InnerS1 {
//Args1とArgs2を同時に展開
//要素数は同じでないとNG
using tuple = std::tuple<std::tuple<Args1, Args2>...>;
};
template <typename... Args2>
struct InnerS2 {
//Args1とArgs2を個別に展開
//要素数は違ってもOK
using tuple = std::tuple<std::tuple<Args1..., Args2...>>;
};
};
int main()
{
using tuple1 = S<int, double>::InnerS1<char,std::string>::tuple;
tuple1 t = { {1, 2.0}, {'a', "abc"} };
std::cout <<
std::get<0>(std::get<0>(t)) << ", " <<
std::get<1>(std::get<0>(t)) << ", " <<
std::get<0>(std::get<1>(t)) << ", " <<
std::get<1>(std::get<1>(t)) << std::endl;
//パラメーターパックの要素数が異なるのでコンパイルエラー
//using tuple2 = S<int, double>::InnerS1<char>::tuple;
//OK
using tuple3 = S<int, double>::InnerS2<char>::tuple;
}
変数テンプレート
C++14からは変数にもテンプレートを適用することができます。
#include <iostream>
template <typename T>
constexpr T pi = static_cast<T>(3.14159265358979323846); //変数テンプレート
//半径から円の面積を返す関数テンプレート
template <typename T>
T calcCircleArea(T radius)
{
return radius * radius * pi<T>; //変数テンプレートの使用
}
int main()
{
std::cout << calcCircleArea(5) << std::endl;
std::cout << calcCircleArea(5.0) << std::endl;
}
75 78.5398
なお、変数テンプレートのデフォルト引数はC++17から使用可能です。
template <class T = int>
T x = T();
int main()
{
//int型
auto a = x<>;
}