再帰テンプレートを用いた std::tuple の展開と処理
本記事では、std::tuple
に対するstd::for_each
的な処理を行うためのコードについて記述する。
以下のコードは、規定クラスのポインタの配列に派生クラスのアドレスを代入し、range-based for
を用いてメンバ仮想関数をコールしている。
struct Base { virtual void operator()() = 0; }; #include <iostream> template<int num> struct Derived final : public Base { void operator()() override final { std::cout << num << std::endl; } }; #include <array> #include <memory> int main() { using namespace std; array<unique_ptr<Base>, 3> a; a[0].reset(new Derived<128>); a[1].reset(new Derived<256>); a[2].reset(new Derived<512>); for (auto & p : a) { p->operator()(); } return 0; }
配列のサイズがコンパイル時定数であれば、一般にstd::vector
を使うよりもstd::array
を使ったほうが良い*1。
更に、上記のコードについて、配列に対する任意の添字について、代入される派生クラスの型が固定なのであれば、わざわざ継承を用いると仮想関数テーブルの分メモリを消費するし、ポインタを介して関数を呼ぶ分処理負荷が増える。
継承を行わないように変更したコードが以下のコードである。
#include <iostream> template<int num> struct Test final { void operator()() { std::cout << num << std::endl; } }; int main() { using namespace std; Test<128> test1; Test<256> test2; Test<512> test3; test1(); test2(); test3(); return 0; }
継承を行っていないので、型が異なるため配列による管理は行えない。ポインタで扱う必要もなくなったので、ローカル変数として宣言している。当然、まとめて扱うことは出来ない。そこで、異なる型をまとめて扱うために、std::tuple
を用いる。
#include <tuple> int main() { using namespace std; auto tpl = make_tuple(Test<128>(), Test<256>(), Test<512>()); get<0>(tpl)(); get<1>(tpl)(); get<2>(tpl)(); return 0; }
std::make_tuple
によって、新しく型が生成され、まとめて扱えている。ただし、要素に対するアクセスはコンパイル時定数で行う必要があるため、一般的に繰り返しに用いられるfor Statement
やwhile Statemanet
では扱うことが出来ない。各要素に対して同じ操作*2を行うためには、再帰テンプレートを用いる必要がある。
#include <tuple> #include <iostream> using namespace std; template<size_t begin, size_t end, bool terminate = begin + 1 == end> struct ExecutePart; template<size_t begin, size_t end> struct ExecutePart<begin, end, true> { template<typename Tuple, typename Function> static void Execute(Tuple && tuple, Function && function) { function(get<begin>(tuple)); } }; template<size_t begin, size_t end> struct ExecutePart<begin, end, false> { template<typename Tuple, typename Function> static void Execute(Tuple && tuple, Function && function) { // execute first half ExecutePart<begin, (begin + end) / 2>::Execute (forward<Tuple>(tuple), forward<Function>(function)); // execute latter half ExecutePart<(begin + end) / 2, end>::Execute (forward<Tuple>(tuple), forward<Function>(function)); } }; // pass all element of tuple to function template<typename Tuple, typename Function> void ExecuteAll(Tuple && tuple, Function && function) { using namespace std; static const size_t end = tuple_size<typename remove_reference<Tuple>::type>::value; ExecutePart<0, end>::Execute( forward<Tuple>(tuple), forward<Function>(function)); } // function object // output template argument template<int num> struct Test final { void operator()() { std::cout << num << std::endl; } }; // function object // call operator() of any type argument struct Function { template<typename T> inline void operator()(T && arg) { arg(); } }; int main() { using namespace std; auto tpl = make_tuple(Test<128>(), Test<256>(),Test<512>()); ExecuteAll(tpl, Function()); return 0; }
右辺値参照やらなんやらかんやらでややこしいが、要は分割統治法を用いてstd::tuple
を各々の要素に分割し、それを関数オブジェクトに対して渡している。各々の要素の型が異なるため、std::functional
や、関数ポインタ、ラムダ式は使えない*3。
ちなみに再帰テンプレートの再帰深度が O(log(n)) になるように組んでいる。
テンプレート関数は部分特殊化出来ないため、再帰呼び出しによってstd::tuple
の展開を行うための関数はテンプレートクラスのメンバ関数になっている。