テンプレート非型メタプログラミング

C++での黒魔術といえばテンプレート型メタプログラミングだ。これは、テンプレートがパターンマッチと再帰を使えることを利用して、型レベルで計算を行う技術を指す。

template<int X>
struct factorial
{
    // 再帰的に展開できる
    static const int value = factorial<X-1>::value * X;
};

// 明示的特殊化によって分岐ができる
template<>
struct factorial<0>
{
    static const int value = 1;
};

本来はテンプレート機能への問題提起として「発見」されたこのTMPは、C++プログラマの格好のおもちゃになり、目先の面白さを超えて多種多様な実践的技法すらも産んだ。

時は流れ、コンパイル時計算がC++プログラミングにおける主要な技法の一つとなり[要出典]、言語規格側のサポートはC++11以降爆発的に充実していった。

C++11は、今更言うまでもないが、まさしくコンパイル時に実行できる関数・コンパイル時に決まる値を作れるconstexprや、標準メタ関数群<type_traits>、テンプレート型エイリアスusing、他にも細かなコンパイル時計算支援の変更が入った。

C++14から変数テンプレートというものが入った。これは、関数や構造体だけでなく値そのものをテンプレートにできるというものだ。cpprefjpのサンプルコードが多分一番わかりやすいのでそのまま引用させていただく。

template <class T>
constexpr T pi = static_cast<T>(3.14159265358979323846);

// 円の面積を求める
template <class T>
T area_of_circle_with_radius(T r)
{
  return pi<T> * r * r;
}

cpprefjp.github.io

C++17ではインライン変数というものが現れた。これによって、ヘッダオンリーなライブラリでも変数を提供できるようになった。

通常、グローバルまたはstaticな変数は定義を必要とする。ヘッダは複数の翻訳単位からincludeされるので、多重定義でリンカを混乱させないため、定義はhppではなくcppファイルに書く必要があった。今では単にinlineと書けば、全部うまく行ってくれる。

cpprefjp.github.io

C++17ではそれ以外にも、非型テンプレート引数をautoで受け取る簡略構文が追加された。これまでは、どの型の変数を取るかを別に指定する(型と値をペアで渡す)か、変数の型を決めておく必要があった。今はもう、その必要はない。

template<auto X>
struct constant_value{};

using one = constant_value<1>;

cpprefjp.github.io

C++20で、非型テンプレートパラメータにユーザー定義クラスも使えるようになった。そのためにはその型はstructural typeである必要がある。これは、n4861の§13.2.7によると

  • scalar type
    • arithmetic type, enum, ポインタ、メンバへのポインタ、nullptr、それらのconst/volatile(§6.8)
  • 左辺値参照
  • {全ての基底クラスと非staticデータメンバ}が{publicであり、かつmutableでない}{structural typeまたはその配列型}であるようなリテラルクラス

クッソ雑に言うと整数、浮動小数点数enum、ポインタでできている全部publicな構造体は許される。ババーン(一切の責任を取りません)

もともと、型の同一性判定のために非型テンプレート引数は「strong equalityで比較可能である」という制約があったのだが、浮動小数点数を許すためにギリギリで変更になったらしい。onihusube9さんに教えていただいた。

ちなみに何一つ関係ないがオニフスベは白くて丸くて大きなキノコだ。小学校にも入らない頃にきのこの図鑑で見て、「こんなきのこもあるのか」と何故か衝撃を受けてそれ以降覚えている。アミガサタケとかの方が衝撃的な形してそうなもんだが。

浮動小数点数の同一性はややこしい。例えば、0/0などによって作られるNaNは自分自身と異なる値になる上に、NaNを表現するビットパターンが異様に広い。具体的に言うと、指数部分のビットが全て1となるものが特殊な値(NaN/Inf)で、小数部分が非ゼロの値だとNaNになる(小数部分が全て0ならInf)。これを利用したNaN Boxingなどの奇妙なテクニックもある。なのでis_same_v<X<nan>, X<nan>>などがどうなるのかは少し気になるところではある。

それはさておき。

ここまでで、

  • 値をテンプレートにできる
  • グローバルな値をヘッダオンリーライブラリで提供できる
  • 非型テンプレート引数をautoで受けられる
  • ユーザー定義型の値を非型テンプレート引数にできる

が揃った。

というわけで、テンプレート型メタプログラミングに換わるC++20時代の黒魔術、テンプレート値メタプログラミングができるようになった。

例えば、以下のようなcons listを考えてみる。C++20以前だと、非型テンプレート引数に勝手な構造体を使うことはできなかったので、要素は型である必要があった。

template<typename Car, typename Cdr>
struct cons{};

// carもcdrも型なので、数値は一度型情報でくるむ必要がある
using list = cons<std::integral_constant<int, 1>,
             cons<std::integral_constant<int, 2>,
             cons<std::integral_constant<int ,3>,
             std::nullptr_t>>>;

整数なら昔から非型テンプレート引数として使えたので、これくらいならいけそうに思えるが、consの要素としてconsが来たときに無理になってしまうのだ。consはユーザー定義型であり、それが非型テンプレート引数として使えるようになるのはC++20以降だからである。

template<auto Car, auto Cdr>
struct cons{};

// C++17ではエラー
using list = cons<1, cons<2, cons<3, nullptr>{}>{}>;

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

prog.cc:6:48: error: non-type template parameters of class type only available with '-std=c++20' or '-std=gnu++20'
    6 | using list = cons<1, cons<2, cons<3, nullptr>{}>{}>;
      |                                                ^
prog.cc:6:51: error: template argument 2 is invalid
    6 | using list = cons<1, cons<2, cons<3, nullptr>{}>{}>;
      |                                                   ^

そしてC++20ではついに、ここまで紹介してきた機能を総動員することによって、以下のように書けるようになる。

template<auto Car, auto Cdr>
struct cons_t{};
template<auto Car, auto Cdr>
inline constexpr cons_t<Car, Cdr> cons;

constexpr auto list = cons<1, cons<2, cons<3, nullptr>>>;

carcdrを取るcons_t型を作ったあと、変数テンプレートとinline変数を使って、cons_t<Car, Cdr>型のconsという値テンプレートを定義する。cons_tは空なのでstructural typeだ。よって、cons_t型の値は非型テンプレート引数として使える。

ついでに、この場合listは型ではなく、値だ。consが値テンプレートだからだ。その型はcons_t<1, cons<2, cons<3, nullptr>>>だ。

以前と比べてみよう。

// C++17
using list = cons<std::integral_constant<int, 1>,
             cons<std::integral_constant<int, 2>,
             cons<std::integral_constant<int ,3>,
             std::nullptr_t>>>;
// C++20
constexpr auto list = cons<1, cons<2, cons<3, nullptr>>>;

シンプル!

追加で、これは値なので、以下のようなこともできる。

template<auto Car, auto Cdr>
std::ostream& operator<<(std::ostream& os,
                         const cons_t<Car, Cdr>&)
{
    os << '(' << Car << ' ' << Cdr << ')';
    return os;
}

std::cout << cons<1, cons<2, cons<3, nullptr>>> << std::endl;

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

(1 (2 (3 nullptr)))

この延長で、コンパイル時非型LISP処理系を書いた。夕食後の時間を2日分使ってしまった。

github.com

これを使うとコンパイル時に走るLISPを書ける。

今はg++-10で動くようにするために文字列定義に丸括弧を使っているが、g++-11(HEAD)では以下のコードが動くようになる。

constexpr auto result = eval<list<while_, 
        list<1, str<"">>, lambda<list<lt, arg<0>, 20>>,
        lambda<list<
        list<plus, arg<0>, 1>,
        list<plus, arg<1>,
            list<if_, list<eq, 0, list<modulus, arg<0>, 15>>, str<"FizzBuzz\n">,
            list<if_, list<eq, 0, list<modulus, arg<0>, 3>>, str<"Fizz\n">,
            list<if_, list<eq, 0, list<modulus, arg<0>, 5>>, str<"Buzz\n">,
                      list<plus, list<to_string, arg<0>>, str<"\n">>
        >>>>
    >>>>;

std::cout << car<cdr<result>> << std::endl;

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19

余裕があるときに細かい部分の実装について書こうと思う。