大学でC++の演習が始まったがテンプレートには触れないようなので触れさせる

テンプレートは C++が闇たる所以だ。 C++に光あれ。しかし、テンプレートを使いこなすことができなければC++C++ではない。そういうものはBetter C (Cよりはマシ) という。大学の演習ではテンプレートについて何もやらないようなので、この記事ではテンプレートの基本からすごく簡単なTMPくらいまで触る。

最初に言っておくと、私はC++の規格書を読んでいないので間違いが含まれる可能性がある。正しくないことを書きたくないのでテンプレートの使い方を紹介するにとどめる。

対象読者は大学でC++の講義や演習が始まったけど、それらでテンプレートを習わないことが確定している大学生だ。

テンプレートの文法

テンプレートは型や整数定数を引数にして雛形 (テンプレート) を作る機能だ。テンプレートは関数、クラス、変数で使用できる。それぞれ関数テンプレート、クラステンプレート、変数テンプレートという。関数、クラス、変数のテンプレートの定義は以下のように行う。

template< テンプレート引数 >
関数, クラス, 変数の定義

例えばクラステンプレートは以下のように定義する。

template<class T>
class hoge {};

テンプレートを実体化するときには以下のように行う。

識別子<テンプレート実引数>

先ほどのclass hogeint型で実体化してみよう。

hoge<int> fuga;   // fugaはhoge<int>型のインスタンス

またコンパイラがテンプレート引数を推論できる場合はテンプレート引数は省略できる。そのような場合を除いてテンプレート引数を省略することはコンパイルエラーにつながる。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)ではTyintに解決される。

しかし、先のような関数では以下のコードはエラーになってしまう。

add(1.5, 3);    // error!

これをコンパイルするためにはテンプレート引数を明示的に渡してやる必要がある。

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"; } // f()と出力するだけ

call_func<f>();                    // fを呼び出す

テンプレートの特殊化

テンプレートの特殊化は特定のテンプレート引数が渡された時、特別の動作を行うことだ。これによってコンパイル時条件分岐などができるようになってしまう。このことはC++の闇と密接にかかわっていると思う。C++に光あれ。

最初にテンプレートの特殊化を使ってintlongで動作が変わる関数を作ってみよう。

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;    // 55が出力される。

関数に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みたいに書くことができる。例えば二つのテンプレート引数TUが同じ型であるかどうかは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);

二つの型が加算可能か確かめる

次はTUが加算可能か確かめるメタ関数を作ろう。加算可能な場合にメンバ定数valuetrueになり、そうでない場合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>型だ。それを継承すればtruefalseの値を持ったメンバ定数valuecan_be_addedから使用可能になる。

型制約

テンプレートに何でもかんでも渡せてしまうのは良くないと思う人は目の付け所がシャープだ (適当) 。実際、整数型しか想定していないのに浮動小数点型を渡されたり、ユーザー定義の構造体を渡されたりするのは恐ろしいエラーメッセージか若しくはバグを生み出すかもしれない。だからテンプレートにおかしな引数をわたしてしまったときにわかりやすくコンパイルエラーになるような仕組みが必要だ。テンプレートに渡す型引数を制限することを型制約という。

型制約にはいろいろな方法があると思う。私は特定の条件を満たした時だけ実体が定義される方法とstd::enable_ifを使う方法しか知らないのでその2種類の方法について解説する。

嫌な奴は定義してやらない方法

クソみたいなテンプレート引数を渡してくる奴がいても安心だ。なぜならば実体化されないからである。例えばintしか渡せない関数テンプレートと、int以外しか渡せない関数テンプレートを書いてみよう。

template<typename Ty>
void int_only();

template<>
void int_only<int>() {}

int_only<long>();  // error! 定義がない
int_only<int>();   // errorじゃない

template<typename Ty>
void ban_int(){}
template<>
void ban_int<int>();

ban_int<int>();  // error!
ban_int<long>(); // errorじゃない

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>(); // error

難しいので、テンプレートの実体化される過程を追ってみよう。

intを渡した場合、std::is_arithmetic_v<Ty>trueになり、その場合、std::enable_if_t<true>* = nullptrvoid* = nullptrになる。これはwell-formedだ。

次に、std::stringを渡してみた場合、std::is_arithmetic_v<Ty>falseになり、その場合、std::enable_if_t<false>* = nullptrは定義されないstd::enable_if<false>::typeを使おうとしているのでコンパイルエラーになる。

テンプレートで遊ぼう。(FizzBuzz)

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構造体だ。これはテンプレートの特殊化をすれば実装できる。

// クラステンプレートFizzBuzz
// 何も特殊化していない
// N : FizzBuzzする値
// D : divisor。Nを割る数
// S : 余り。NをDで割った余り
template<int N, int D, int S>
struct FizzBuzz {
    static constexpr std::string_view value = "";
};
// Nが1で割り切れる場合、メンバ定数valueがNの値を取るFizzBuzz構造体
// メンバ定数valueは基底クラスで定義されている。
template<int N>
struct FizzBuzz<N, 1, 0> : public std::integral_constant<int, N> {};

// Nが3で割り切れない場合の特殊化
template<int N, int S>
struct FizzBuzz<N, 3, S> : public FizzBuzz<N, 1, 0> {};

// Nが3で割り切れる場合の特殊化
// valueが”Fizz"svである。
template<int N>
struct FizzBuzz<N, 3, 0> {
    static constexpr std::string_view value = "Fizz";
};

// Nが5で割り切れない場合は3で割ってみて、FizzBuzz<N, 3, 0>かFizzBuzz<N, 3, S>を継承する。
// その結果メンバ定数valueが"Fizz"svかNの値を取る。
template<int N, int S>
struct FizzBuzz<N, 5, S> : public FizzBuzz<N, 3, N % 3> {};

// Nが5で割り切れる場合の特殊化
template<int N>
struct FizzBuzz<N, 5, 0> {
    static constexpr std::string_view value = "Buzz";
};

// Nが15で割り切れない場合、Nを5で割ってみる。
template<int N, int S>
struct FizzBuzz<N, 15, S> : public FizzBuzz<N, 5, N % 5>{};

// Nが15で割り切れる場合の特殊化。
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ヘッダにあるメタ関数を使いこなせるようにならなければならない。