テンプレートは C++が闇たる所以だ。 C++に光あれ。しかし、テンプレートを使いこなすことができなければC++はC++ではない。そういうものはBetter C (Cよりはマシ) という。大学の演習ではテンプレートについて何もやらないようなので、この記事ではテンプレートの基本からすごく簡単なTMPくらいまで触る。
最初に言っておくと、私はC++の規格書を読んでいないので間違いが含まれる可能性がある。正しくないことを書きたくないのでテンプレートの使い方を紹介するにとどめる。
対象読者は大学でC++の講義や演習が始まったけど、それらでテンプレートを習わないことが確定している大学生だ。
テンプレートの文法
テンプレートは型や整数定数を引数にして雛形 (テンプレート) を作る機能だ。テンプレートは関数、クラス、変数で使用できる。それぞれ関数テンプレート、クラステンプレート、変数テンプレートという。関数、クラス、変数のテンプレートの定義は以下のように行う。
template< テンプレート引数 >
関数, クラス, 変数の定義
例えばクラステンプレートは以下のように定義する。
template<class T>
class hoge {};
テンプレートを実体化するときには以下のように行う。
識別子<テンプレート実引数>
先ほどのclass hoge
をint
型で実体化してみよう。
hoge<int> fuga;
またコンパイラがテンプレート引数を推論できる場合はテンプレート引数は省略できる。そのような場合を除いてテンプレート引数を省略することはコンパイルエラーにつながる。C++17では関数とクラスのテンプレート引数を推論することができる場合がある。クラステンプレートの実引数推論はdeduction-guide (推論補助) を書くことでコンストラクタにテンプレート引数で渡された型がなくても推論することができる場合がある。
テンプレートの初歩
様々な型に適用したい処理をテンプレートで書くことで、関数オーバーロードや、扱う型や定数だけが異なる別のクラスと変数を自分で書く必要がなくなる。例えば二つの数値を足し算するプログラムをテンプレートを使って書こう。
C言語では以下のように書かざるを得なかった。
int add_int(int a, int b) { return a + b; }
long add_long(long a, long b) { return a + b; }
Cではオーバーロードすらできないので愚直に違う名前の関数を並べる必要がある。はっきり言ってこんなものはクソだ。C++でこれをもっと簡単に書くことができる。
template<typename Ty>
Ty add(Ty a, Ty b) { return a + b; }
たったこれだけでint
, long
, double
などなど、足し算を適用できるすべての型についてadd
関数を適用できる。さらに、上の関数はテンプレート実引数をコンパイラが推論できる。例えばadd(1,1)
ではTy
はint
に解決される。
しかし、先のような関数では以下のコードはエラーになってしまう。
add(1.5, 3);
これをコンパイルするためにはテンプレート引数を明示的に渡してやる必要がある。
add<double>(1.5, 3);
テンプレートは型だけでなく整数定数も引数にできる。整数定数は何もint
型とは限らない。関数ポインタやヌルポインタもコンパイル時定数なのでテンプレート引数になり得る。テンプレート引数で渡された整数値をプリントする関数を実装してみよう。
template<int Int>
void print_num() { std::cout << Int << std::endl; }
print_num<5>();
次に関数ポインタを受け取ってみよう。
template<void(*FuncPtr)()>
void call_func() { (*FuncPtr)(); }
void f() { std::cout << "f()\n"; }
call_func<f>();
テンプレートの特殊化
テンプレートの特殊化は特定のテンプレート引数が渡された時、特別の動作を行うことだ。これによってコンパイル時条件分岐などができるようになってしまう。このことはC++の闇と密接にかかわっていると思う。C++に光あれ。
最初にテンプレートの特殊化を使ってint
とlong
で動作が変わる関数を作ってみよう。
template<typename Integral>
void f() {}
template<>
void f<int>() { std::cout << "int"; }
template<>
void f<long>() { std::cout << "long"; }
int
が渡されたら"int"
を出力し、long
なら"long"
、ほかの型ならば何もしない関数の出来上がりだ。この関数に利用価値は無いが、テンプレートの特殊化は様々な可能性を秘めた魅力的な機能だということを解ってもらうためちょっと難しいこともやってみよう。
テンプレートの特殊化を使ってちょっとした計算を行ってみよう。1からNまでの和を求める関数でテンプレートの特殊化を学ぶ。
template<unsigned int N>
unsigned int sum() { return N+sum<N-1>(); }
template<>
unsigned int sum<1>() { return 1; }
ただしこれは実行時に関数呼び出しなどを行っているため、せっかくのテンプレートの特殊化の利点 (コンパイル時条件分岐) がほぼ無意味になっている。どうせなら実際の計算などもコンパイル時に行い、結果をコンパイル時定数にしたいものだ。関数にconstexpr
をつければ目的は達成できるが、もうすこしテンプレートの特殊化について学びたい人は次のコードも読んでほしい。
template<unsigned int N>
struct sum {
static constexpr unsigned int value = N + sum<N-1>::value;
};
template<>
struct sum<1> {
static constexpr unsigned int value = 1;
};
std::cout << sum<10>::value << std::endl;
関数にconstexpr
を付けたほうが簡単に書けるが、こういったイディオムはconstexpr
が無かったり、制約が厳しかった時代のC++でよく見るので、古いコードを読む機会が多い人は見ておくと良いだろう。また、このイディオムはvalue
(値)だけでなくtype
(型)にも応用できるので、メタプログラミングが好きな人は見る機会が多いと思う。
では早速、今のイディオムを使って型をプログラミングしよう。標準ライブラリにあるいくつかの型特性のクラスは自分でも実装できるくらい簡単だ。例えば引数の型にポインタを付けるプログラムは以下のように実装できる。
template<typename Ty>
struct add_pointer {
typedef Ty* type;
};
template<typename Ty>
struct add_pointer<Ty&> {
typedef Ty* type;
};
template<typename Ty>
struct add_pointer<Ty&&> {
typedef Ty* type;
};
static_assert(std::is_same_v<int*, add_pointer<int>::type>);
static_assert(std::is_same_v<int*, add_pointer<int&>::type>);
static_assert(std::is_same_v<int*, add_pointer<int&&>::type>);
参照のポインタは宣言できないためテンプレートの特殊化で参照を取り除いたTy
に対してポインタを付与している。テンプレートは、このように「型をプログラミング」することができる。
標準ライブラリみたいなかっこいいメタ関数を作る
標準ライブラリは型特性を問い合わせるときにis_xxxx
みたいに書くことができる。例えば二つのテンプレート引数T
とU
が同じ型であるかどうかはstd::is_same
という構造体を使って確かめる。こういうのは思ったより簡単に作ることができる。
is_same
まず初めに二つの型が同じかどうか調べるis_same
を作ってみよう。といってもこれはものすごく簡単だ。テンプレートの特殊化をするだけだ。
template<class T, class U>
struct is_same : std::bool_constant<false> {};
template<class T>
struct is_same<T, T> : std::bool_constant<true> {};
static_assert(is_same<int, int>::value);
static_assert(!is_same<int, double>::value);
二つの型が加算可能か確かめる
次はT
とU
が加算可能か確かめるメタ関数を作ろう。加算可能な場合にメンバ定数value
がtrue
になり、そうでない場合false
になる。メタ関数は二つの部分に分かれる。実装とインターフェースだ。
struct can_be_added_impl {
template<class ...Args>
static auto check(...)
->std::bool_constant<false>;
template<class T, class U>
static auto check(T*, U*)
->decltype(std::declval<T>() + std::declval<U>(), std::bool_constant<true>{});
};
template<class T, class U>
struct can_be_added : decltype(can_be_added_impl::check<T, U>(nullptr, nullptr)) {};
static_assert(can_be_added<int, int>::value);
static_assert(can_be_added<int, double>::value);
static_assert(!can_be_added<int, std::string>::value);
can_be_added<T, U>
はcan_be_added_impl::check<T, U>
関数の戻り値の型を継承する。check
関数はT
型とU
型の値が+
演算子で加算できるとき二番目のcheck
関数に解決され、それ以外の場合では最初のcheck
関数に解決される。二番目のcheck
関数の戻り値の型はstd::bool_constant<true>
型で、最初のはstd::bool_constant<false>
型だ。それを継承すればtrue
かfalse
の値を持ったメンバ定数value
がcan_be_added
から使用可能になる。
型制約
テンプレートに何でもかんでも渡せてしまうのは良くないと思う人は目の付け所がシャープだ (適当) 。実際、整数型しか想定していないのに浮動小数点型を渡されたり、ユーザー定義の構造体を渡されたりするのは恐ろしいエラーメッセージか若しくはバグを生み出すかもしれない。だからテンプレートにおかしな引数をわたしてしまったときにわかりやすくコンパイルエラーになるような仕組みが必要だ。テンプレートに渡す型引数を制限することを型制約という。
型制約にはいろいろな方法があると思う。私は特定の条件を満たした時だけ実体が定義される方法とstd::enable_if
を使う方法しか知らないのでその2種類の方法について解説する。
嫌な奴は定義してやらない方法
クソみたいなテンプレート引数を渡してくる奴がいても安心だ。なぜならば実体化されないからである。例えばint
しか渡せない関数テンプレートと、int
以外しか渡せない関数テンプレートを書いてみよう。
template<typename Ty>
void int_only();
template<>
void int_only<int>() {}
int_only<long>();
int_only<int>();
template<typename Ty>
void ban_int(){}
template<>
void ban_int<int>();
ban_int<int>();
ban_int<long>();
std::enable_if
を使う方法
クソみたいなテンプレート引数を渡してくるやつがいても安心だ。なぜならばstd::enable_if
がいるからである。std::enable_if
はクラステンプレートだ。最初の引数がtrue
の場合、メンバ型type
が定義される。type
は第二引数で指定できるが、指定しなかった場合はvoid
になる。
最初の引数に満たすべき条件を書いて、type
が定義されている場合で有効なコードを書いておくことで、条件が満たされなかった場合はコンパイルエラーになってくれる。
今度はTy
が算術型である場合にwell-formedにしてみよう。
template<typename Ty, std::enable_if_t<std::is_arithmetic_v<Ty>>* = nullptr>
void arithmetic() {}
arithmetic<int>();
arithmetic<std::string>();
難しいので、テンプレートの実体化される過程を追ってみよう。
int
を渡した場合、std::is_arithmetic_v<Ty>
はtrue
になり、その場合、std::enable_if_t<true>* = nullptr
がvoid* = nullptr
になる。これはwell-formedだ。
次に、std::string
を渡してみた場合、std::is_arithmetic_v<Ty>
はfalse
になり、その場合、std::enable_if_t<false>* = nullptr
は定義されないstd::enable_if<false>::type
を使おうとしているのでコンパイルエラーになる。
FizzBuzzというのは3の倍数の時だけアホになる世界のナベアツさんのように、3の倍数の時にFizzといい、5の倍数の時にBuzzという遊びだ。アメリカでは長距離ドライブなどの最中でFizzBuzzをやるらしい。もちろんコンパイル時FizzBuzzではなく、人間が順番に数字をいう中でFizzとかBuzzとか言うだけだ。
Aさん「1」
Bさん「2」
Aさん「Fizz」
Bさん「4」
という風にやるが、プログラミング界隈でのFizzBuzzは、コードを書けないプログラマ志願者を見分ける方法として有名だ。今回はそれをコンパイル時にやる。
コンパイル時FizzBuzzをやるためにまず必要なのはテンプレート実引数が3の倍数の時に"Fizz”sv
、5の倍数の時に"Buzz"sv
、15の倍数の時に”FizzBuzz"sv
、それ以外でその数字になるようなメンバ定数value
を持つFizzBuzz構造体だ。これはテンプレートの特殊化をすれば実装できる。
template<int N, int D, int S>
struct FizzBuzz {
static constexpr std::string_view value = "";
};
template<int N>
struct FizzBuzz<N, 1, 0> : public std::integral_constant<int, N> {};
template<int N, int S>
struct FizzBuzz<N, 3, S> : public FizzBuzz<N, 1, 0> {};
template<int N>
struct FizzBuzz<N, 3, 0> {
static constexpr std::string_view value = "Fizz";
};
template<int N, int S>
struct FizzBuzz<N, 5, S> : public FizzBuzz<N, 3, N % 3> {};
template<int N>
struct FizzBuzz<N, 5, 0> {
static constexpr std::string_view value = "Buzz";
};
template<int N, int S>
struct FizzBuzz<N, 15, S> : public FizzBuzz<N, 5, N % 5>{};
template<int N>
struct FizzBuzz<N, 15, 0> {
static constexpr std::string_view value = "FizzBuzz";
};
テンプレート引数にはコンパイル時定数しか渡せないのでfor文で上のFizzBuzz
構造体のvalue
を順番に出力することはできない。ループが使えないなら再帰を使えばいいじゃない。
FizzBuzz
構造体のvalue
に順番にアクセスするために正確には再帰ではないが、再帰っぽいことをしてみよう。
template<int N>
struct Run {
static void run() {
run<N - 1>::run();
std::cout << FizzBuzz<N, 15, N%15>::value << '\n';
}
};
template<>
struct Run<1> {
static void run() {
std::cout << FizzBuzz<1, 15, 1%15>::value << '\n';
}
};
int main(){
Run<100>::run();
}
これでコンパイル時FizzBuzzは完了だ。Wandboxで動作を確認できる。
wandbox.org
あとは標準ライブラリを使いこなすだけ!
テンプレートは闇だが、使いこなすことができればC++で表現の幅が広がる。ほとんどの処理をコンパイル時にすることも出来るようになるだろう。そのためにはまず標準ライブラリのtype_traits
ヘッダにあるメタ関数を使いこなせるようにならなければならない。