Skip to content

Latest commit

 

History

History
107 lines (79 loc) · 6.4 KB

template.md

File metadata and controls

107 lines (79 loc) · 6.4 KB

テンプレート

C++ テンプレートは、コードジェネレータであると同時にコンパイル時計算の手段でもあります。 後者の役割は、テンプレートを使って実現するには不便すぎて一部の物好きのための機能としか思えませんでしたが、その役割は constexpr が担いつつあるようなので、ここでは今後も典型的に使われるであろう前者の役割に注目します。

出来ること

テンプレートは、型を引数として受けとって、クラス(型)や関数をコンパイル時に生成するものです。 型を具体的に示さないまま、クラスや関数の内容を記述することによってテンプレートを定義できます。 テンプレートに実際の型を渡して実体化するときに、その型が持っていない機能をテンプレートの内部で使っていたら、 コンパイルできずにエラーになります。

使い過ぎに注意

ベンチマークなどで設定パラメータを複数用意して実験することがあります。 それぞれのパラメータ毎に if 文で動的に条件分岐するのは性能に影響が出そうだから嫌だな、と思ったことがあって、パラメータを最内ループで分岐させるのではなく、main 関数の最初で全部分岐させてしまい、それぞれのパラメータセットに特化したコードを実行するように、全部テンプレート引数として実装してみました。 例えば以下のようなコードです:

tamplate <int A, int B, int C>
void disaptch3(const Option& opt)
{
    // A, B, C を条件として分岐する。

    // 条件分岐済みのコードが生成される。

}

template <int A, int B>
void dispatch2(const Option& opt)
{
    switch (opt.c) {
    case C1:
        dispatch3<A, B, C1>(opt); break;
    case C2:
        dispatch3<A, B, C2>(opt); break;
    // ...
    }
}

template <int A>
void dispatch1(const Option& opt)
{
    switch (opt.b) {
    case B1:
        dispatch2<A, B1>(opt); break;
    case B2:
        dispatch2<A, B2>(opt); break;
    // ...
    }
}

void dispatch0(const Option& opt)
{
    switch (opt.a) {
    case A1:
        dispatch1<A1>(opt); break;
    case A2:
        dispatch1<A2>(opt); break;
    // ...
    }
}

この例だとパラメータは 3 つですが、実際にはパラメータの数は 5 つくらいで、それぞれ 4 個くらいの値が存在していたでしょうか。 ・・・どうなったか皆さんもうお分かりですね? 4 の 5 乗個 = 1024 個程の似て異なるコードがテンプレートから生成され、本来なら数秒で済んでいたコンパイル時間が 10 分を越えたあげく、バイナリサイズが巨大になってしまいました。 このように、簡単に組み合わせ爆発をするテンプレートを書けてしまえますが、手に負えません。 このような用途では、Just-in-Time コンパイルの手段を模索するか、CPU の分岐予測を信じて通常の if 文を使うしかないでしょう。

パーフェクトフォワーディング

パーフェクトフォワーディング (完全転送) は、lvalue reference で受ける実装と rvalue reference で受ける実装をオーバーロードしている関数があったとき、それをまとめてテンプレートで扱う仕組みです。 2 つのテンプレートをそれぞれ書けばいいじゃないかという意見もあるでしょうが、 呼び出しが n 段になると 2 の n 乗個定義しないといけなくなり、実装が実質不可能になるので、このような機能が欲しくなるようです。

やり方は簡単です:

  1. 型引数にして (ここでは仮に T とします)、
  2. 自らは T&& で受け (const とか付けたらダメです)、
  3. std::forward で包んでオーバーロードしている関数に渡す

これだけです。 何故このような動作になるかを知りたい人は、他の資料か C++ 仕様を参照ください。 私は知識が足りなくてうまく説明できませんが、このような挙動を実現したい人達が、関数やテンプレートのシグネチャマッチングルールなどをうまく設定したのだと思います。

テンプレートの限界

C++ のテンプレートをコンパイル時計算(メタプログラミング)のための言語と見做した場合、それは関数型プログラミング言語のような特徴を持っています。 繰り返し処理は再帰を使わないと実現できないだとか、条件分岐もテンプレートの特殊化によるマッチングなどを利用して行われるとか。 元々は、複数型に対して同じ処理をするコードをまとめて記述したい、という素朴な目的で作られたものが、結果として、ほぼチューリング完全の能力を、あくまで私の主観ですが、使いやすいとは言えない状態で獲得してしまったように思います。 C++ のような手続型スタイルで、繰り返し処理や条件分岐をコンパイル時に実行し、コード生成する、という形のメタプログラミング言語だったら、もっと使い勝手の良いものだったんじゃないかと思います。 同じことを思う人は C++ 標準策定に関わる人達にもいるようで、 C++11 で登場した constexpr や C++17 で導入された if constexpr (これは完全な形でのコンパイル時条件分岐ではないのが悲しいです)などが、 より使いやすいメタプログラミング手段の萌芽に見え、ひそかに期待するところではあります。

テンプレートを駆使する方へ

ここまで読んでも「俺/僕/私は C++テンプレートを極めるんだ!」という勇敢な方や「C++ テンプレート駆使しないとどうやらやりたいことが出来ないようだ。。。」という気の毒な方は頑張ってください。 私はあまり詳しくないので良い情報を提供できませんが、いくつか日本語でも書籍は出ているようです。 boost template metaplogramming にも便利?なライブラリがあります。