C++でコンパイル時に偶数/奇数の配列が欲しかった

ので、テンプレートでメタプログラミングしてみた。

コンパイル時という事でconstexprを使えばいいのですが、いかんせん欲しかった環境がVS2015だったので(C++14のconstexprの制限緩和に非対応)、TMPで生成することにしました。戦略としては、コンパイル時整数シーケンスを生成し、そこからパラメータパック展開を利用して配列初期化を行います。
という事で、まずは任意のシーケンスから配列への変換を考えます。

template<typename SequenceType>
struct make_static_array;

template<template<class T, T...> typename SequenceType, typename ElementType, ElementType... Sequence>
struct make_static_array<SequenceType<ElementType, Sequence...>> {
	static constexpr std::size_t N = sizeof...(Sequence);
	static constexpr ElementType Array[]{ Sequence... };
};

//make_static_array<std::integer_sequence<int, 1, 3, 5, 7>>::Array
//みたいに使う

シーケンスとしてはstd::integer_sequenceの形を想定、おそらくたいてい同じ形になっているはず。クラステンプレートの部分特殊化を利用してstd::integer_sequence<int, 1, 2,3, ... >のような形をしている型をパラメータに受け取れるようにします。可変長テンプレートで肝心の整数列を受けてそれを配列の初期化子リストで展開することで配列を生成します。ついでにシーケンスの要素数も取得しておきます。

次に、これを使って偶数/奇数配列を生成するクラスを考えましょう。シーケンスを作る部分はとりあえず置いておきます。

template <int Size, int Start = 1>
struct static_odd_array : make_static_array<typename make_integer_sequence<Start, 2, Size>::type> {};

template <int Size, int Start = 0>
struct static_even_array : make_static_array<typename make_integer_sequence<Start, 2, Size>::type> {};

整数シーケンスを作る部分をmake_integer_sequenceとして仮置きしておきます。先ほどのmake_static_arrayに奇数/偶数シーケンスを保持するクラスを渡した上で継承することで、これらのクラスはmake_static_arrayの二つのメンバを持つことになります(メタ関数転送とか言うらしい)。
ここで、奇数か偶数の整数シーケンスを作るメタ関数の概形を考えてみます。奇数か偶数で異なる関数を作るのは綺麗じゃないので、引数によって生成するシーケンスが変化した方がよく、どうせならより汎用的に使えた方がいいでしょう。また、欲しい整数の個数を渡せばそのサイズのシーケンスを生成してほしいです。奇数/偶数の列は初期値を与えてそこから+2づつしていけば得られ、初期値の偶奇によって列の偶奇は決まります。なので、引数として必要なのは、配列サイズと初期値、おまけに増分(要素間の差)でしょうか。これを踏まえたうえで作ってみます。

template<int Bias, typename IntegralSequence, bool IsOdd = false, int Last = 0>
struct make_integer_sequence_impl;

//サイズが偶数の時によばれる
template<int Bias, template<class T, T...> typename SequenceType, int... Sequence>
struct make_integer_sequence_impl<Bias, SequenceType<int, Sequence...>> {
	using type = std::integer_sequence<int, Sequence..., (Sequence + Bias)... >;
};

//サイズが奇数の時によばれる
template<int Bias, int Last, template<class T, T...> typename SequenceType, int... Sequence>
struct make_integer_sequence_impl<Bias, SequenceType<int, Sequence...>, true, Last> {
	using type = std::integer_sequence<int, Sequence..., (Sequence + Bias)..., Last>; //末尾に呼び出し側で生成された最後尾要素を追加する
};

template<int End, int Step, int Size, typename = void>
struct make_integer_sequence;

//サイズが偶数の時
template<int End, int Step, int Size>
struct make_integer_sequence<End, Step, Size, std::enable_if_t<Size % 2 == 0>>
	: make_integer_sequence_impl<Size / 2 * Step, typename make_integer_sequence<End, Step, Size / 2>::type>
{};

//サイズが奇数の時
template<int End, int Step, int Size>
struct make_integer_sequence<End, Step, Size, std::enable_if_t<Size % 2 == 1>>
	: make_integer_sequence_impl<Size / 2 * Step, typename make_integer_sequence<End, Step, Size / 2>::type, true, End + (Size - 1) * Step>
{};

//サイズが1になった時
template<int End, int Step>
struct make_integer_sequence<End, Step, 1>
{
	using type = std::integer_sequence<int, End>;
};

//サイズが0の時
template<int End, int Step>
struct make_integer_sequence<End, Step, 0>
{
	using type = std::integer_sequence<int>;
};

めっちゃ長くなりました。名前はいいのが思いつかなかったのでstd::make_integer_sequenceともろ被りしました、適切な名前空間で包みましょう・・・。End=初期値、Step=増分、Size=要素数、の値を受け取ってSize個のEndから始まってStep飛びの整数シーケンスを生成します。

何をやっているのかというと、まずは再帰的に配列サイズを半分に分割していきます。サイズが1になったところで、与えられた初期値を入れたサイズ1のシーケンスを作り、そのシーケンスに与えられたステップを足したサイズ2の列を作り、そのシーケンスから各要素にステップ×2を足したサイズ4の列を作り、・・・というように一番上まで戻りシーケンスの生成が完了します。
ただし問題として、初期値によっては分割後のサイズが奇数になることがあります。その場合は一番最後の要素をその場で生成しサイズを偶数にしてやります。

//サイズが偶数の時
template<int End, int Step, int Size>
struct make_integer_sequence<End, Step, Size, std::enable_if_t<Size % 2 == 0>>
	: make_integer_sequence_impl<Size / 2 * Step, typename make_integer_sequence<End, Step, Size / 2>::type>
{};

//サイズが奇数の時
template<int End, int Step, int Size>
struct make_integer_sequence<End, Step, Size, std::enable_if_t<Size % 2 == 1>>
	: make_integer_sequence_impl<Size / 2 * Step, typename make_integer_sequence<End, Step, Size / 2>::type, true, End + (Size - 1) * Step>
{};

各段は自分が分割の途中なのか最初なのかを知りませんが、自分が生成すべきシーケンスの数と一番最初の値及び増分は分かりますので、一番最後の値が”初期値 + 増分×(要素数-1)”となる事が分かります(要素数-1となっているのは初期値を含めた数だからです。例えば、0から始まって1飛びの100個の値の一番最後の値は100ではなく99になります)。
これらの事をmake_integer_sequence関数が行います。部分特殊化とenable_ifを使ってサイズの偶奇とサイズが1か0になった時の分岐を行っています。

実際の整数列を作成し、::typeに保持するのはmake_integer_sequence_implという安直な名前の関数が行います。ここもやはり部分特殊化をふんだんに利用しています。make_integer_sequenceはmake_integer_sequence_implを継承し、引数の値によって呼び出すオーバーロードを切り替えているのです。make_integer_sequence_implは引数に、Bias=その段でのバイアス分(前半分から後ろ半分を生成するためのバイアス)、SequenceType=一段下のシーケンス(前半分のシーケンス)を受け取ります。

template<int Bias, typename IntegralSequence, bool IsOdd = false, int Last = 0>
struct make_integer_sequence_impl;

//サイズが偶数の時によばれる
template<int Bias, template<class T, T...> typename SequenceType, int... Sequence>
struct make_integer_sequence_impl<Bias, SequenceType<int, Sequence...>> {
	using type = std::integer_sequence<int, Sequence..., (Sequence + Bias)... >;
};

//サイズが奇数の時によばれる
template<int Bias, int Last, template<class T, T...> typename SequenceType, int... Sequence>
struct make_integer_sequence_impl<Bias, SequenceType<int, Sequence...>, true, Last> {
	using type = std::integer_sequence<int, Sequence..., (Sequence + Bias)..., Last>; //末尾に呼び出し側で生成された最後尾要素を追加する
};

make_integer_sequence_implの二つのオーバーロードはそれぞれサイズが偶数、奇数となった時に行う処理を分けるためにあります。その段でのサイズが奇数であった場合は、trueと最後尾要素を受け取り分岐しますが、デフォルトテンプレート引数を使って偶数の時はそれらの指定を省略します。
make_integer_sequence_implの受け取る前半分のシーケンスとは、make_integer_sequence::type、つまりは再帰的に呼び出されているため呼び出し時点では値が決定していません。サイズ1になるまで下降していって、そこから上に戻ってくるわけです。

ある段において前半分のシーケンスの値が確定したならばそのシーケンスの各値にバイアスを足して後ろ半分を生成します。バイアスは前段のシーケンスの最初の要素に足すことで、前段シーケンス最後尾+Step、の値にならなければなりません。このバイアスは”その段でのサイズ*Step”になる事が分かり、この値を前段各要素に足すことで後段の要素が得られる事が分かるでしょう。このように、各段において生成するべき数の半分のシーケンスから残りを導出することが出来ます。ちなみに、生成時は可変長テンプレートのパック展開を利用しています。

template<int Bias, template<class T, T...> typename SequenceType, int... Sequence>
struct make_integer_sequence_impl<Bias, SequenceType<int, Sequence...>> {
	using type = std::integer_sequence<int, Sequence..., (Sequence + Bias)... >; //ここで生成している
};

std::integer_sequenceをシーケンス一時保存用に使い、その引数に”<前半分列..., (前半分列 + バイアス)... >”のように展開します。(前半分列 + バイアス)...のような書き方は前半分列の各要素にバイアスを足しながら展開してね?、という事です。

そもそもなぜにこんなに複雑な事をするかというと、テンプレート再帰回数を減らすためです。何も考えずに再起させれば、もっと簡単にN個のシーケンスを得ることが出来ますが、N回再帰することにもなります。何が問題かと言えばそのような再帰回数には制限がある事です(clangのデフォルトは1024回)。オーダー表記すればO(N)で、今回のような二分法を使えばO(logN)になります。

というわけで、どうやら完成したらしいですね。テストと再帰回数が本当にlogNになっているのかのチェックとコード全文を兼ねて、wandboxでの実行結果を貼っておきます。
wandbox.org
ちなみにStartを変えれば負の数からでも偶数/奇数シーケンスを生成可能です。static_odd_arrayとstatic_even_arrayのStart引数はstatic_assert等で制限した方がいいかもしれません。

とても疲れましたが、とても楽しかった、こういうのだけ書いていたいなあ・・・・。


参考文献:
index_tuple イディオムにおける index_range の効率的な実装 - ボレロ村上 - ENiyGmaA Code
C++のパラメータパック基礎&パック展開テクニック