素敵なタイトルが思いつきませんでした。
結論
以下のリンクに書いてある内容について触れています
c++ - Pretty-print std::tuple - Stack Overflow
c++ - Iterating on a tuple... again - Stack Overflow
c++ - What does this variadic template code do? - Stack Overflow
std::integer_sequence - cppreference.com
- インデックスアクセス可能なシーケンスの各要素にindex_sequenceを用いて戻り値voidな関数を適用するにはunpack可能な文脈で関数を呼び出し、カンマ演算子を用いて式の型を変える
本編前の前提
std::index_sequence
C++14からstd::make_index_sequence
やstd::index_sequence
などが追加されました。
std::index_sequence
はstd::size_t
型のコンパイル時整数列を表します。std::make_index_sequence
は、例えばstd::make_index_sequence<3UL>{}
と書くとstd::index_sequence<0UL, 1UL, 2UL>
のインスタンスを生成する便利関数です。
index accessable sequenceとvariadic template
C++11まではvariadic templateの各引数に何らかの処理を適用していくには再帰処理を使うしかありませんでした。*1
C++14からはstd::index_sequence
がありますので、コンパイル時に長さの決まっているインデックアクセス可能なシーケンス(std::tuple
やstd::array
など)は、可変長引数関数テンプレートを再帰せず、インデックスアクセスで各要素になんらかの処理を適用することができるようになりました。テンプレートの再帰深度などの問題を緩和できるため、C++14以降で、コンパイル時に長さが決まる添え字アクセス可能なシーケンスの各要素に何か処理を行いたい場合は、再帰関数ではなくstd::index_sequence
を用いた実装を推奨します。
map/transformの実装
例えば、std::array
の各要素に変換を加えて、新しい配列を作りたいとします。ほかの言語で言うmap、C++標準で言うstd::transform
のような関数の実装を考えます。使い方のイメージは以下のようになります:
std::array<int, 3> const arr = {{1, 2, 3}}; std::array<std::string, 3> const arr2 = transform(arr, [](int elem) { return std::to_string(elem) + std::to_string(elem); }); for (auto const& e : arr2) { std::cout << e << std::endl; }
実行結果イメージ
11 22 33
では、実装を考えてみたいと思います。まず便利メソッドとしてmake_array
というメソッドを作成します。簡易版なので説明は省略します:
template <typename ...Args, std::size_t N = sizeof...(Args)> std::array<typename std::common_type<Args...>::type, N> make_array(Args&&... args) { return {{std::forward<Args>(args)...}}; }
これを使うと先ほどのtransform
の使用イメージ例のコードは以下のようになります:
auto const arr = make_array(1, 2, 3); auto const arr2 = transform(arr, [](int elem) { return std::to_string(elem) + std::to_string(elem); }); // make_array関係なく元々autoにできる for (auto const& e : arr2) { std::cout << e << std::endl; }
transformの実装は以下のようになります:
template <typename T, std::size_t N, typename F, std::size_t ...Indices> auto transform_impl(std::array<T, N> const& arr, F&& f, std::index_sequence<Indices...>) { return make_array(std::forward<F>(f)(arr[Indices])...); } template <typename T, std::size_t N, typename F> auto transform(std::array<T, N> const& arr, F&& f) { return transform_impl(arr, std::forward<F>(f), std::make_index_sequence<N>{}); }
std::make_index_sequence
を使うことで、0からN-1までの連続した整数リストを得ることができます。この整数列のパラメータパックを展開する際に添え字として利用しています。そして、arr[0]
から、arr[N-1]
までの各要素にf
を適用し、その戻り値の配列を作成しています。この際、f
の戻り値型はstd::common_type
で求められる型リストに限られます。
本編
for_eachの実装
さて、ここからが本編です。先程の例のように戻り値の配列のようなunpackが許される場所での展開は問題なく行えるのですが、transform
と同様の実装でstd::for_each
のような処理(戻り値がvoid
で、副作用(例えば標準出力など)を期待した関数など)を適用したい場合、実装に工夫が要ります。なぜならば配列の各要素に関数を適用した結果はvoid
であり、これはunpackができる場所(関数の引数や、配列の初期化リストなど)では許されない型だからです。
template <typename T, std::size_t N, typename F, std::size_t ...Indices> auto for_each_impl(std::array<T, N> const& arr, F&& f, std::index_sequence<Indices...>) { return make_array(std::forward<F>(f)(arr[Indices])...); // fの戻り値がvoidだとエラー // std::forward<F>(f)(arr[Indices])...; // こういう風にも書けない } template <typename T, std::size_t N, typename F> auto for_each(std::array<T, N> const& arr, F&& f) { return for_each_impl(arr, std::forward<F>(f), std::make_index_sequence<N>{}); }
工夫するポイントはパラメータパックが展開できる場所で、「戻り値のvoidを潰して代わりに何らかの値を返す式」の値のリストにする、です。何を言っているのか分からないと思いますので、実装を示します:
配列の初期化リストを使う方法
template <typename T, typename F, std::size_t N, std::size_t ...Indices> void for_each_impl(std::array<T, N> const& arr, F&& f, std::index_sequence<Indices...>) { int unused[] = {0, (static_cast<void>(std::forward<F>(f)(arr[Indices])), 0)...}; static_cast<void>(unused); } template <typename T, std::size_t N, typename F> void for_each(std::array<T, N> const& arr, F&& f) { for_each_impl(arr, std::forward<F>(f), std::make_index_sequence<N>{}); }
for_each
はtransform
と同様に、impl関数に転送しています。impl関数の中ではパラメータパックを展開できる場所として配列の初期化リストを使用しています。impl関数中の(static_cast<void>(std::forward<F>(f)(arr[Indices])), 0)
に注目します。C++ではカンマ演算子で複数の式が並べられた場合、カンマ演算子の一番右にある式が全体の型/値になります。
int a = (nullptr, 3); // 右辺の式全体は型: int, 値: 3
という訳で(static_cast<void>(std::forward<F>(f)(arr[Indices])), 0)
は式全体はint型、値0になります。ただし注意があります。operator,
はオーバーロード可能なので、組み込みのoperator,
の場合は上記の説明通りですが、オーバーロードされている場合はこの通りになりません。f
の適用をvoidでキャストしているのは式の値を使わないことを明示するためです。ここで(static_cast<void>(std::forward<F>(f)(arr[Indices])), 0)
をINVOKE_FUNC
とします。INVOKE_FUNC
は戻り値がintで値は0になります:
int unused[] = {0, INVOKE_FUNC...};
これで、int unused[]
の初期化リストが正しいリストのように見えるかと思います。初期化リストの先頭になぜ0が1つあるかというと、仮にint unused[] = {INVOKE_FUNC}
と書いた場合、可変長テンプレート引数が0の時、for_each_impl
は
int unused[] = {};
となってしまうからです。unusedの長さが0になってしまいます。C++では長さ0の配列は許されず、コンパイルエラーになってしまいます。そこで初期化リストの先頭に1つ要素を用意することでint ununsed[]
の長さを最低1と保証しているわけです。
最後にunused
をvoid
にキャストしているのは、コンパイラの未使用の変数警告を抑制するためです。通常ならば使用していない変数がある場合警告を出すべきですが、今回はパラメータパックの展開の都合上登場した変数で、使わずとも意味があるため、この警告は適切ではありません。*2
そのため、コンパイラにこの変数は評価しない、ということを明示するためvoid
にキャストしています。
このfor_eachは以下のように使用できます:
auto const arr = make_array(1, 2, 3); for_each(arr, [](auto const& elem) { std::cout << elem << std::endl; });
関数呼び出しを使う方法
(3/12追記 @Linda_ppさんありがとうございます)
前項では配列の初期化リストを用いた方法を示しましたが、関数呼び出しを利用する方法もあります。for_each
の実装は前項と同じなので、for_each_impl
の実装を示します:
template <typename T, typename F, std::size_t N, std::size_t ...Indices> void for_each_impl(std::array<T, N> const& arr, F&& f, std::index_sequence<Indices...>) { [](auto&&...){}((static_cast<void>(std::forward<F>(f)(arr[Indices])), 0)...); }
こちらの方法ではC++14から入ったジェネリックラムダ式を用いた可変長テンプレート引数をもったラムダ式を定義し、その場で呼び出しています。こちらの方法では未評価変数の登場と使用がなく、関数パラメータのカンマ区切りはoperator,
の心配もなくなります((f(arr[Indices], 0)
のoperator,
のオーバーロードの可能性は残っています。INVOKE_FUNC, INVOKE_FUNC
の間のoperator,
について触れています))
長くなってしまいましたが、以上です。